Merge pull request #239 from christianlouis/copilot/update-tests-external-apis
Fix DeepSource issues: lazy logging, static methods, unused vars, reimports
This commit is contained in:
@@ -3,6 +3,7 @@ Diagnostic API endpoints
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
|
||||||
@@ -14,10 +15,12 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
CurrentUser = Annotated[dict, Depends(get_current_user)]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/diagnostic/settings")
|
@router.get("/diagnostic/settings")
|
||||||
@require_login
|
@require_login
|
||||||
async def diagnostic_settings(request: Request, current_user: dict = Depends(get_current_user)):
|
async def diagnostic_settings(request: Request, current_user: CurrentUser):
|
||||||
"""
|
"""
|
||||||
API endpoint to dump settings to the log and view basic config information
|
API endpoint to dump settings to the log and view basic config information
|
||||||
This endpoint doesn't expose sensitive information like passwords or tokens
|
This endpoint doesn't expose sensitive information like passwords or tokens
|
||||||
|
|||||||
+14
-13
@@ -4,6 +4,7 @@ Dropbox API endpoints
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from fastapi import APIRouter, Form, HTTPException, Request, status
|
from fastapi import APIRouter, Form, HTTPException, Request, status
|
||||||
@@ -22,11 +23,11 @@ router = APIRouter()
|
|||||||
@require_login
|
@require_login
|
||||||
async def exchange_dropbox_token(
|
async def exchange_dropbox_token(
|
||||||
request: Request,
|
request: Request,
|
||||||
client_id: str = Form(...),
|
client_id: Annotated[str, Form(...)],
|
||||||
client_secret: str = Form(...),
|
client_secret: Annotated[str, Form(...)],
|
||||||
redirect_uri: str = Form(...),
|
redirect_uri: Annotated[str, Form(...)],
|
||||||
code: str = Form(...),
|
code: Annotated[str, Form(...)],
|
||||||
folder_path: str = Form(None),
|
folder_path: Annotated[Optional[str], Form()] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Exchange an authorization code for a refresh token from Dropbox.
|
Exchange an authorization code for a refresh token from Dropbox.
|
||||||
@@ -58,10 +59,10 @@ async def exchange_dropbox_token(
|
|||||||
@require_login
|
@require_login
|
||||||
async def update_dropbox_settings(
|
async def update_dropbox_settings(
|
||||||
request: Request,
|
request: Request,
|
||||||
app_key: str = Form(None),
|
refresh_token: Annotated[str, Form(...)],
|
||||||
app_secret: str = Form(None),
|
app_key: Annotated[Optional[str], Form()] = None,
|
||||||
refresh_token: str = Form(...),
|
app_secret: Annotated[Optional[str], Form()] = None,
|
||||||
folder_path: str = Form(None),
|
folder_path: Annotated[Optional[str], Form()] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update Dropbox settings in memory
|
Update Dropbox settings in memory
|
||||||
@@ -182,10 +183,10 @@ async def test_dropbox_token(request: Request):
|
|||||||
@require_login
|
@require_login
|
||||||
async def save_dropbox_settings(
|
async def save_dropbox_settings(
|
||||||
request: Request,
|
request: Request,
|
||||||
app_key: str = Form(None),
|
refresh_token: Annotated[str, Form(...)],
|
||||||
app_secret: str = Form(None),
|
app_key: Annotated[Optional[str], Form()] = None,
|
||||||
refresh_token: str = Form(...),
|
app_secret: Annotated[Optional[str], Form()] = None,
|
||||||
folder_path: str = Form(None),
|
folder_path: Annotated[Optional[str], Form()] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Save Dropbox settings to the .env file
|
Save Dropbox settings to the .env file
|
||||||
|
|||||||
+12
-10
@@ -6,7 +6,7 @@ import logging
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from typing import List, Optional
|
from typing import Annotated, List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, UploadFile
|
from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, UploadFile
|
||||||
from sqlalchemy import asc, desc
|
from sqlalchemy import asc, desc
|
||||||
@@ -27,6 +27,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
DbSession = Annotated[Session, Depends(get_db)]
|
||||||
|
|
||||||
|
|
||||||
def get_limiter():
|
def get_limiter():
|
||||||
"""Get the limiter from app state."""
|
"""Get the limiter from app state."""
|
||||||
@@ -39,7 +41,7 @@ def get_limiter():
|
|||||||
@require_login
|
@require_login
|
||||||
def list_files_api(
|
def list_files_api(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: DbSession,
|
||||||
page: int = Query(1, ge=1, description="Page number"),
|
page: int = Query(1, ge=1, description="Page number"),
|
||||||
per_page: int = Query(50, ge=1, le=200, description="Items per page"),
|
per_page: int = Query(50, ge=1, le=200, description="Items per page"),
|
||||||
sort_by: str = Query(
|
sort_by: str = Query(
|
||||||
@@ -152,7 +154,7 @@ def _get_file_processing_status(db: Session, file_id: int) -> dict:
|
|||||||
|
|
||||||
@router.get("/files/{file_id}")
|
@router.get("/files/{file_id}")
|
||||||
@require_login
|
@require_login
|
||||||
def get_file_details(request: Request, file_id: int, db: Session = Depends(get_db)):
|
def get_file_details(request: Request, file_id: int, db: DbSession):
|
||||||
"""
|
"""
|
||||||
Get detailed information about a specific file including processing history.
|
Get detailed information about a specific file including processing history.
|
||||||
"""
|
"""
|
||||||
@@ -205,7 +207,7 @@ def get_file_details(request: Request, file_id: int, db: Session = Depends(get_d
|
|||||||
|
|
||||||
@router.delete("/files/{file_id}")
|
@router.delete("/files/{file_id}")
|
||||||
@require_login
|
@require_login
|
||||||
def delete_file_record(request: Request, file_id: int, db: Session = Depends(get_db)):
|
def delete_file_record(request: Request, file_id: int, db: DbSession):
|
||||||
"""
|
"""
|
||||||
Delete a file record from the database.
|
Delete a file record from the database.
|
||||||
This only removes the database entry, not the actual file.
|
This only removes the database entry, not the actual file.
|
||||||
@@ -240,7 +242,7 @@ def delete_file_record(request: Request, file_id: int, db: Session = Depends(get
|
|||||||
|
|
||||||
@router.post("/files/bulk-delete")
|
@router.post("/files/bulk-delete")
|
||||||
@require_login
|
@require_login
|
||||||
def bulk_delete_files(request: Request, file_ids: List[int], db: Session = Depends(get_db)):
|
def bulk_delete_files(request: Request, file_ids: List[int], db: DbSession):
|
||||||
"""
|
"""
|
||||||
Delete multiple file records from the database.
|
Delete multiple file records from the database.
|
||||||
This only removes the database entries, not the actual files.
|
This only removes the database entries, not the actual files.
|
||||||
@@ -284,7 +286,7 @@ def bulk_delete_files(request: Request, file_ids: List[int], db: Session = Depen
|
|||||||
|
|
||||||
@router.post("/files/bulk-reprocess")
|
@router.post("/files/bulk-reprocess")
|
||||||
@require_login
|
@require_login
|
||||||
def bulk_reprocess_files(request: Request, file_ids: List[int], db: Session = Depends(get_db)):
|
def bulk_reprocess_files(request: Request, file_ids: List[int], db: DbSession):
|
||||||
"""
|
"""
|
||||||
Reprocess multiple files by queuing them for processing.
|
Reprocess multiple files by queuing them for processing.
|
||||||
"""
|
"""
|
||||||
@@ -345,7 +347,7 @@ def bulk_reprocess_files(request: Request, file_ids: List[int], db: Session = De
|
|||||||
|
|
||||||
@router.post("/files/{file_id}/reprocess")
|
@router.post("/files/{file_id}/reprocess")
|
||||||
@require_login
|
@require_login
|
||||||
def reprocess_single_file(request: Request, file_id: int, db: Session = Depends(get_db)):
|
def reprocess_single_file(request: Request, file_id: int, db: DbSession):
|
||||||
"""
|
"""
|
||||||
Reprocess a single file by queuing it for processing again.
|
Reprocess a single file by queuing it for processing again.
|
||||||
|
|
||||||
@@ -487,10 +489,10 @@ def _retry_pipeline_step(file_record: FileRecord, step_name: str, db: Session) -
|
|||||||
def retry_subtask(
|
def retry_subtask(
|
||||||
request: Request,
|
request: Request,
|
||||||
file_id: int,
|
file_id: int,
|
||||||
|
db: DbSession,
|
||||||
subtask_name: str = Query(
|
subtask_name: str = Query(
|
||||||
..., description="Name of the subtask to retry (e.g., 'upload_to_dropbox', 'extract_metadata_with_gpt')"
|
..., description="Name of the subtask to retry (e.g., 'upload_to_dropbox', 'extract_metadata_with_gpt')"
|
||||||
),
|
),
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Retry a specific failed subtask for a file.
|
Retry a specific failed subtask for a file.
|
||||||
@@ -603,8 +605,8 @@ def retry_subtask(
|
|||||||
def get_file_preview(
|
def get_file_preview(
|
||||||
request: Request,
|
request: Request,
|
||||||
file_id: int,
|
file_id: int,
|
||||||
|
db: DbSession,
|
||||||
version: str = Query("original", description="original or processed"),
|
version: str = Query("original", description="original or processed"),
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get file content for preview (original or processed version).
|
Get file content for preview (original or processed version).
|
||||||
@@ -675,8 +677,8 @@ def get_file_preview(
|
|||||||
def download_file(
|
def download_file(
|
||||||
request: Request,
|
request: Request,
|
||||||
file_id: int,
|
file_id: int,
|
||||||
|
db: DbSession,
|
||||||
version: str = Query("original", description="original or processed"),
|
version: str = Query("original", description="original or processed"),
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Download file (original or processed version) as attachment.
|
Download file (original or processed version) as attachment.
|
||||||
|
|||||||
+16
-16
@@ -5,7 +5,7 @@ Google Drive API endpoints
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Form, HTTPException, Request, status
|
from fastapi import APIRouter, Form, HTTPException, Request, status
|
||||||
|
|
||||||
@@ -23,11 +23,11 @@ router = APIRouter()
|
|||||||
@require_login
|
@require_login
|
||||||
async def exchange_google_drive_token(
|
async def exchange_google_drive_token(
|
||||||
request: Request,
|
request: Request,
|
||||||
client_id: str = Form(...),
|
client_id: Annotated[str, Form(...)],
|
||||||
client_secret: str = Form(...),
|
client_secret: Annotated[str, Form(...)],
|
||||||
redirect_uri: str = Form(...),
|
redirect_uri: Annotated[str, Form(...)],
|
||||||
code: str = Form(...),
|
code: Annotated[str, Form(...)],
|
||||||
folder_id: Optional[str] = Form(None),
|
folder_id: Annotated[Optional[str], Form()] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Exchange an authorization code for refresh and access tokens from Google.
|
Exchange an authorization code for refresh and access tokens from Google.
|
||||||
@@ -59,11 +59,11 @@ async def exchange_google_drive_token(
|
|||||||
@require_login
|
@require_login
|
||||||
async def update_google_drive_settings(
|
async def update_google_drive_settings(
|
||||||
request: Request,
|
request: Request,
|
||||||
client_id: str = Form(None),
|
refresh_token: Annotated[str, Form(...)],
|
||||||
client_secret: str = Form(None),
|
client_id: Annotated[Optional[str], Form()] = None,
|
||||||
refresh_token: str = Form(...),
|
client_secret: Annotated[Optional[str], Form()] = None,
|
||||||
folder_id: str = Form(None),
|
folder_id: Annotated[Optional[str], Form()] = None,
|
||||||
use_oauth: str = Form("true"),
|
use_oauth: Annotated[str, Form()] = "true",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update Google Drive settings in memory
|
Update Google Drive settings in memory
|
||||||
@@ -328,11 +328,11 @@ def format_time_remaining(time_delta):
|
|||||||
@require_login
|
@require_login
|
||||||
async def save_dropbox_settings(
|
async def save_dropbox_settings(
|
||||||
request: Request,
|
request: Request,
|
||||||
client_id: str = Form(None),
|
refresh_token: Annotated[str, Form(...)],
|
||||||
client_secret: str = Form(None),
|
client_id: Annotated[Optional[str], Form()] = None,
|
||||||
refresh_token: str = Form(...),
|
client_secret: Annotated[Optional[str], Form()] = None,
|
||||||
folder_id: str = Form(None),
|
folder_id: Annotated[Optional[str], Form()] = None,
|
||||||
use_oauth: str = Form("true"),
|
use_oauth: Annotated[str, Form()] = "true",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Save Google Drive settings to the .env file
|
Save Google Drive settings to the .env file
|
||||||
|
|||||||
+6
-4
@@ -3,7 +3,7 @@ Processing logs API endpoints
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||||
from sqlalchemy import desc
|
from sqlalchemy import desc
|
||||||
@@ -18,12 +18,14 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
DbSession = Annotated[Session, Depends(get_db)]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/logs")
|
@router.get("/logs")
|
||||||
@require_login
|
@require_login
|
||||||
def list_processing_logs(
|
def list_processing_logs(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: DbSession,
|
||||||
file_id: Optional[int] = Query(None, description="Filter by file ID"),
|
file_id: Optional[int] = Query(None, description="Filter by file ID"),
|
||||||
task_id: Optional[str] = Query(None, description="Filter by task ID"),
|
task_id: Optional[str] = Query(None, description="Filter by task ID"),
|
||||||
limit: int = Query(100, ge=1, le=1000, description="Number of logs to return"),
|
limit: int = Query(100, ge=1, le=1000, description="Number of logs to return"),
|
||||||
@@ -81,7 +83,7 @@ def list_processing_logs(
|
|||||||
|
|
||||||
@router.get("/logs/file/{file_id}")
|
@router.get("/logs/file/{file_id}")
|
||||||
@require_login
|
@require_login
|
||||||
def get_file_processing_logs(request: Request, file_id: int, db: Session = Depends(get_db)):
|
def get_file_processing_logs(request: Request, file_id: int, db: DbSession):
|
||||||
"""
|
"""
|
||||||
Get all processing logs for a specific file.
|
Get all processing logs for a specific file.
|
||||||
Returns logs ordered by timestamp (oldest first to show processing flow).
|
Returns logs ordered by timestamp (oldest first to show processing flow).
|
||||||
@@ -125,7 +127,7 @@ def get_file_processing_logs(request: Request, file_id: int, db: Session = Depen
|
|||||||
|
|
||||||
@router.get("/logs/task/{task_id}")
|
@router.get("/logs/task/{task_id}")
|
||||||
@require_login
|
@require_login
|
||||||
def get_task_processing_logs(request: Request, task_id: str, db: Session = Depends(get_db)):
|
def get_task_processing_logs(request: Request, task_id: str, db: DbSession):
|
||||||
"""
|
"""
|
||||||
Get all processing logs for a specific task.
|
Get all processing logs for a specific task.
|
||||||
Returns logs ordered by timestamp (oldest first to show processing flow).
|
Returns logs ordered by timestamp (oldest first to show processing flow).
|
||||||
|
|||||||
+16
-15
@@ -5,6 +5,7 @@ OneDrive API endpoints
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from fastapi import APIRouter, Form, HTTPException, Request, status
|
from fastapi import APIRouter, Form, HTTPException, Request, status
|
||||||
@@ -23,11 +24,11 @@ router = APIRouter()
|
|||||||
@require_login
|
@require_login
|
||||||
async def exchange_onedrive_token(
|
async def exchange_onedrive_token(
|
||||||
request: Request,
|
request: Request,
|
||||||
client_id: str = Form(...),
|
client_id: Annotated[str, Form(...)],
|
||||||
client_secret: str = Form(...),
|
client_secret: Annotated[str, Form(...)],
|
||||||
redirect_uri: str = Form(...),
|
redirect_uri: Annotated[str, Form(...)],
|
||||||
code: str = Form(...),
|
code: Annotated[str, Form(...)],
|
||||||
tenant_id: str = Form(...),
|
tenant_id: Annotated[str, Form(...)],
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Exchange an authorization code for a refresh token.
|
Exchange an authorization code for a refresh token.
|
||||||
@@ -197,11 +198,11 @@ def format_time_remaining(time_delta):
|
|||||||
@require_login
|
@require_login
|
||||||
async def save_onedrive_settings(
|
async def save_onedrive_settings(
|
||||||
request: Request,
|
request: Request,
|
||||||
client_id: str = Form(None),
|
refresh_token: Annotated[str, Form(...)],
|
||||||
client_secret: str = Form(None),
|
client_id: Annotated[Optional[str], Form()] = None,
|
||||||
refresh_token: str = Form(...),
|
client_secret: Annotated[Optional[str], Form()] = None,
|
||||||
tenant_id: str = Form("common"),
|
tenant_id: Annotated[str, Form()] = "common",
|
||||||
folder_path: str = Form(None),
|
folder_path: Annotated[Optional[str], Form()] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Save OneDrive settings to the .env file
|
Save OneDrive settings to the .env file
|
||||||
@@ -292,11 +293,11 @@ async def save_onedrive_settings(
|
|||||||
@require_login
|
@require_login
|
||||||
async def update_onedrive_settings(
|
async def update_onedrive_settings(
|
||||||
request: Request,
|
request: Request,
|
||||||
client_id: str = Form(None),
|
refresh_token: Annotated[str, Form(...)],
|
||||||
client_secret: str = Form(None),
|
client_id: Annotated[Optional[str], Form()] = None,
|
||||||
refresh_token: str = Form(...),
|
client_secret: Annotated[Optional[str], Form()] = None,
|
||||||
tenant_id: str = Form("common"),
|
tenant_id: Annotated[str, Form()] = "common",
|
||||||
folder_path: str = Form(None),
|
folder_path: Annotated[Optional[str], Form()] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update OneDrive settings in memory (without modifying .env file)
|
Update OneDrive settings in memory (without modifying .env file)
|
||||||
|
|||||||
+11
-11
@@ -3,7 +3,7 @@ API endpoints for managing application settings.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
from typing import Annotated, Any, Dict, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -39,6 +39,10 @@ def require_admin(request: Request) -> dict:
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
DbSession = Annotated[Session, Depends(get_db)]
|
||||||
|
AdminUser = Annotated[dict, Depends(require_admin)]
|
||||||
|
|
||||||
|
|
||||||
class SettingUpdate(BaseModel):
|
class SettingUpdate(BaseModel):
|
||||||
"""Model for updating a setting"""
|
"""Model for updating a setting"""
|
||||||
|
|
||||||
@@ -63,7 +67,7 @@ class SettingsListResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=SettingsListResponse)
|
@router.get("/", response_model=SettingsListResponse)
|
||||||
async def get_settings(request: Request, db: Session = Depends(get_db), admin: dict = Depends(require_admin)):
|
async def get_settings(request: Request, db: DbSession, admin: AdminUser):
|
||||||
"""
|
"""
|
||||||
Get all application settings with metadata.
|
Get all application settings with metadata.
|
||||||
Admin only.
|
Admin only.
|
||||||
@@ -89,7 +93,7 @@ async def get_settings(request: Request, db: Session = Depends(get_db), admin: d
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/{key}", response_model=SettingResponse)
|
@router.get("/{key}", response_model=SettingResponse)
|
||||||
async def get_setting(key: str, request: Request, db: Session = Depends(get_db), admin: dict = Depends(require_admin)):
|
async def get_setting(key: str, request: Request, db: DbSession, admin: AdminUser):
|
||||||
"""
|
"""
|
||||||
Get a specific setting by key.
|
Get a specific setting by key.
|
||||||
Admin only.
|
Admin only.
|
||||||
@@ -114,8 +118,8 @@ async def update_setting(
|
|||||||
key: str,
|
key: str,
|
||||||
setting: SettingUpdate,
|
setting: SettingUpdate,
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: DbSession,
|
||||||
admin: dict = Depends(require_admin),
|
admin: AdminUser,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update a specific setting.
|
Update a specific setting.
|
||||||
@@ -156,9 +160,7 @@ async def update_setting(
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/{key}")
|
@router.delete("/{key}")
|
||||||
async def delete_setting(
|
async def delete_setting(key: str, request: Request, db: DbSession, admin: AdminUser):
|
||||||
key: str, request: Request, db: Session = Depends(get_db), admin: dict = Depends(require_admin)
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Delete a setting from the database (reverts to environment variable or default).
|
Delete a setting from the database (reverts to environment variable or default).
|
||||||
Admin only.
|
Admin only.
|
||||||
@@ -182,9 +184,7 @@ async def delete_setting(
|
|||||||
|
|
||||||
|
|
||||||
@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: Session = Depends(get_db), admin: dict = Depends(require_admin)
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Update multiple settings at once.
|
Update multiple settings at once.
|
||||||
Admin only.
|
Admin only.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pytest-asyncio>=0.23.0
|
|||||||
pytest-mock>=3.12.0
|
pytest-mock>=3.12.0
|
||||||
httpx>=0.26.0 # For async test client
|
httpx>=0.26.0 # For async test client
|
||||||
testcontainers>=3.7.1 # For integration tests with real containers
|
testcontainers>=3.7.1 # For integration tests with real containers
|
||||||
|
fpdf2>=2.8.0 # For generating test PDF documents in integration tests
|
||||||
minio>=7.1.0 # For MinIO/S3 integration tests
|
minio>=7.1.0 # For MinIO/S3 integration tests
|
||||||
redis>=4.5.0 # For Redis integration tests
|
redis>=4.5.0 # For Redis integration tests
|
||||||
boto3>=1.26.0 # For S3 integration tests
|
boto3>=1.26.0 # For S3 integration tests
|
||||||
|
|||||||
+52
-1
@@ -4,7 +4,7 @@ Pytest configuration and shared fixtures for DocuElevate tests.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from typing import Generator
|
from typing import Dict, Generator, Optional
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
@@ -12,6 +12,37 @@ from sqlalchemy import create_engine
|
|||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
|
# Capture original environment variables before overriding with test defaults.
|
||||||
|
# This allows integration tests to detect when real API credentials are available
|
||||||
|
# (e.g., injected via GitHub Actions secrets) and run live API verification.
|
||||||
|
_EXTERNAL_API_ENV_KEYS = [
|
||||||
|
"OPENAI_API_KEY",
|
||||||
|
"OPENAI_BASE_URL",
|
||||||
|
"AZURE_AI_KEY",
|
||||||
|
"AZURE_ENDPOINT",
|
||||||
|
"AZURE_REGION",
|
||||||
|
"AWS_ACCESS_KEY_ID",
|
||||||
|
"AWS_SECRET_ACCESS_KEY",
|
||||||
|
"S3_BUCKET_NAME",
|
||||||
|
"S3_FOLDER_PREFIX",
|
||||||
|
"DROPBOX_APP_KEY",
|
||||||
|
"DROPBOX_APP_SECRET",
|
||||||
|
"DROPBOX_REFRESH_TOKEN",
|
||||||
|
"ONEDRIVE_CLIENT_ID",
|
||||||
|
"ONEDRIVE_CLIENT_SECRET",
|
||||||
|
"ONEDRIVE_REFRESH_TOKEN",
|
||||||
|
"ONEDRIVE_TENANT_ID",
|
||||||
|
"ONEDRIVE_FOLDER_PATH",
|
||||||
|
"GOOGLE_DRIVE_CREDENTIALS_JSON",
|
||||||
|
"GOOGLE_DRIVE_FOLDER_ID",
|
||||||
|
"AUTHENTIK_CLIENT_ID",
|
||||||
|
"AUTHENTIK_CLIENT_SECRET",
|
||||||
|
"AUTHENTIK_CONFIG_URL",
|
||||||
|
"SESSION_SECRET",
|
||||||
|
]
|
||||||
|
_PLACEHOLDER_VALUES = {"test-key", "test", "", "NOT_SET"}
|
||||||
|
_original_env: Dict[str, Optional[str]] = {key: os.environ.get(key) for key in _EXTERNAL_API_ENV_KEYS}
|
||||||
|
|
||||||
# Set test environment variables before importing app
|
# Set test environment variables before importing app
|
||||||
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
|
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
|
||||||
os.environ["REDIS_URL"] = "redis://localhost:6379/1"
|
os.environ["REDIS_URL"] = "redis://localhost:6379/1"
|
||||||
@@ -167,6 +198,26 @@ def mock_azure_response():
|
|||||||
return {"analyzeResult": {"content": "Test document content extracted by OCR", "pages": [{"pageNumber": 1}]}}
|
return {"analyzeResult": {"content": "Test document content extracted by OCR", "pages": [{"pageNumber": 1}]}}
|
||||||
|
|
||||||
|
|
||||||
|
def has_real_env(*keys: str) -> bool:
|
||||||
|
"""Check if real (non-placeholder) environment variables were set before test overrides.
|
||||||
|
|
||||||
|
Returns True only if ALL specified keys had non-placeholder values in the
|
||||||
|
original environment. Used by integration tests to decide whether to skip
|
||||||
|
when real credentials are unavailable.
|
||||||
|
"""
|
||||||
|
for key in keys:
|
||||||
|
value = _original_env.get(key)
|
||||||
|
if value is None or value in _PLACEHOLDER_VALUES:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def original_env() -> Dict[str, Optional[str]]:
|
||||||
|
"""Provide access to the original environment variables captured before test overrides."""
|
||||||
|
return dict(_original_env)
|
||||||
|
|
||||||
|
|
||||||
# Markers for categorizing tests
|
# Markers for categorizing tests
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
"""Configure custom pytest markers."""
|
"""Configure custom pytest markers."""
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ This module provides fixtures for spinning up real infrastructure components:
|
|||||||
|
|
||||||
These tests exercise the full application stack end-to-end.
|
These tests exercise the full application stack end-to-end.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import pytest
|
import pytest
|
||||||
@@ -24,6 +25,8 @@ from testcontainers.postgres import PostgresContainer
|
|||||||
from testcontainers.redis import RedisContainer
|
from testcontainers.redis import RedisContainer
|
||||||
from testcontainers.minio import MinioContainer
|
from testcontainers.minio import MinioContainer
|
||||||
|
|
||||||
|
_TEST_CREDENTIAL = "testpass" # noqa: S105
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def postgres_container() -> Generator:
|
def postgres_container() -> Generator:
|
||||||
@@ -88,9 +91,7 @@ def gotenberg_container() -> Generator:
|
|||||||
"""
|
"""
|
||||||
container = DockerContainer("gotenberg/gotenberg:8")
|
container = DockerContainer("gotenberg/gotenberg:8")
|
||||||
container.with_exposed_ports(3000)
|
container.with_exposed_ports(3000)
|
||||||
container.with_command(
|
container.with_command("gotenberg --chromium-disable-javascript=false --chromium-allow-list=file:///.*")
|
||||||
"gotenberg --chromium-disable-javascript=false --chromium-allow-list=file:///.*"
|
|
||||||
)
|
|
||||||
|
|
||||||
container.start()
|
container.start()
|
||||||
time.sleep(5) # Gotenberg takes a bit longer to start
|
time.sleep(5) # Gotenberg takes a bit longer to start
|
||||||
@@ -121,7 +122,7 @@ def webdav_container() -> Generator:
|
|||||||
container.with_exposed_ports(80)
|
container.with_exposed_ports(80)
|
||||||
container.with_env("AUTH_TYPE", "Basic")
|
container.with_env("AUTH_TYPE", "Basic")
|
||||||
container.with_env("USERNAME", "testuser")
|
container.with_env("USERNAME", "testuser")
|
||||||
container.with_env("PASSWORD", "testpass")
|
container.with_env("PASSWORD", _TEST_CREDENTIAL)
|
||||||
|
|
||||||
container.start()
|
container.start()
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
@@ -135,7 +136,7 @@ def webdav_container() -> Generator:
|
|||||||
"host": host,
|
"host": host,
|
||||||
"port": port,
|
"port": port,
|
||||||
"username": "testuser",
|
"username": "testuser",
|
||||||
"password": "testpass",
|
"password": _TEST_CREDENTIAL,
|
||||||
}
|
}
|
||||||
|
|
||||||
container.stop()
|
container.stop()
|
||||||
@@ -151,7 +152,7 @@ def sftp_container() -> Generator:
|
|||||||
container = DockerContainer("atmoz/sftp:latest")
|
container = DockerContainer("atmoz/sftp:latest")
|
||||||
container.with_exposed_ports(22)
|
container.with_exposed_ports(22)
|
||||||
# Create user: username:password:uid:gid:directory
|
# Create user: username:password:uid:gid:directory
|
||||||
container.with_command("testuser:testpass:1001:1001:upload")
|
container.with_command(f"testuser:{_TEST_CREDENTIAL}:1001:1001:upload")
|
||||||
|
|
||||||
container.start()
|
container.start()
|
||||||
time.sleep(3) # SFTP server needs time to initialize
|
time.sleep(3) # SFTP server needs time to initialize
|
||||||
@@ -164,7 +165,7 @@ def sftp_container() -> Generator:
|
|||||||
"host": host,
|
"host": host,
|
||||||
"port": port,
|
"port": port,
|
||||||
"username": "testuser",
|
"username": "testuser",
|
||||||
"password": "testpass",
|
"password": _TEST_CREDENTIAL,
|
||||||
"folder": "/home/testuser/upload",
|
"folder": "/home/testuser/upload",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +206,7 @@ def ftp_container() -> Generator:
|
|||||||
container.with_exposed_ports(21, 30000, 30001, 30002, 30003, 30004)
|
container.with_exposed_ports(21, 30000, 30001, 30002, 30003, 30004)
|
||||||
container.with_env("PUBLICHOST", "localhost")
|
container.with_env("PUBLICHOST", "localhost")
|
||||||
container.with_env("FTP_USER_NAME", "testuser")
|
container.with_env("FTP_USER_NAME", "testuser")
|
||||||
container.with_env("FTP_USER_PASS", "testpass")
|
container.with_env("FTP_USER_PASS", _TEST_CREDENTIAL)
|
||||||
container.with_env("FTP_USER_HOME", "/home/testuser")
|
container.with_env("FTP_USER_HOME", "/home/testuser")
|
||||||
|
|
||||||
container.start()
|
container.start()
|
||||||
@@ -219,7 +220,7 @@ def ftp_container() -> Generator:
|
|||||||
"host": host,
|
"host": host,
|
||||||
"port": port,
|
"port": port,
|
||||||
"username": "testuser",
|
"username": "testuser",
|
||||||
"password": "testpass",
|
"password": _TEST_CREDENTIAL,
|
||||||
"folder": "/",
|
"folder": "/",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -9,6 +9,8 @@ from starlette.responses import RedirectResponse
|
|||||||
|
|
||||||
from app.auth import get_current_user, get_gravatar_url, require_login
|
from app.auth import get_current_user, get_gravatar_url, require_login
|
||||||
|
|
||||||
|
_TEST_CREDENTIAL = "test" # noqa: S105
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
class TestGetCurrentUser:
|
class TestGetCurrentUser:
|
||||||
@@ -217,7 +219,7 @@ class TestAuthEndpoints:
|
|||||||
|
|
||||||
def test_auth_post_not_available_when_auth_disabled(self, client):
|
def test_auth_post_not_available_when_auth_disabled(self, client):
|
||||||
"""Test that POST /auth returns 404 when auth is disabled."""
|
"""Test that POST /auth returns 404 when auth is disabled."""
|
||||||
response = client.post("/auth", data={"username": "admin", "password": "test"})
|
response = client.post("/auth", data={"username": "admin", "password": _TEST_CREDENTIAL})
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ End-to-end integration tests using real infrastructure.
|
|||||||
These tests spin up actual services (PostgreSQL, Redis, Gotenberg, WebDAV, SFTP, MinIO)
|
These tests spin up actual services (PostgreSQL, Redis, Gotenberg, WebDAV, SFTP, MinIO)
|
||||||
and test the complete application workflow from API request to file upload.
|
and test the complete application workflow from API request to file upload.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import pytest
|
import pytest
|
||||||
@@ -13,6 +14,13 @@ from unittest.mock import patch
|
|||||||
# Import testcontainers requirement
|
# Import testcontainers requirement
|
||||||
pytest.importorskip("testcontainers", reason="testcontainers not installed")
|
pytest.importorskip("testcontainers", reason="testcontainers not installed")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import psycopg2 # noqa: F401
|
||||||
|
|
||||||
|
_has_psycopg2 = True
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
_has_psycopg2 = False
|
||||||
|
|
||||||
from tests.fixtures_integration import (
|
from tests.fixtures_integration import (
|
||||||
postgres_container,
|
postgres_container,
|
||||||
redis_container,
|
redis_container,
|
||||||
@@ -26,6 +34,8 @@ from tests.fixtures_integration import (
|
|||||||
db_session_real,
|
db_session_real,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_TEST_CREDENTIAL = "pass" # noqa: S105
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
@pytest.mark.requires_docker
|
@pytest.mark.requires_docker
|
||||||
@@ -52,8 +62,10 @@ class TestEndToEndWithRedis:
|
|||||||
"""
|
"""
|
||||||
from app.tasks.upload_to_webdav import upload_to_webdav
|
from app.tasks.upload_to_webdav import upload_to_webdav
|
||||||
|
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
# Configure to use real WebDAV server
|
# Configure to use real WebDAV server
|
||||||
mock_settings.webdav_url = webdav_container["url"] + "/"
|
mock_settings.webdav_url = webdav_container["url"] + "/"
|
||||||
@@ -86,9 +98,7 @@ class TestEndToEndWithRedis:
|
|||||||
file_url = f"{webdav_container['url']}/{filename}"
|
file_url = f"{webdav_container['url']}/{filename}"
|
||||||
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
file_url,
|
file_url, auth=(webdav_container["username"], webdav_container["password"]), timeout=5
|
||||||
auth=(webdav_container["username"], webdav_container["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -123,13 +133,10 @@ class TestEndToEndWithRedis:
|
|||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings:
|
with patch("app.tasks.upload_to_webdav.settings") as mock_settings:
|
||||||
mock_settings.webdav_url = "http://test.com"
|
mock_settings.webdav_url = "http://test.com"
|
||||||
mock_settings.webdav_username = "user"
|
mock_settings.webdav_username = "user"
|
||||||
mock_settings.webdav_password = "pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
|
|
||||||
# This will queue the task in Redis
|
# This will queue the task in Redis
|
||||||
result = upload_to_webdav.apply_async(
|
result = upload_to_webdav.apply_async(args=["/tmp/test.txt"], kwargs={"file_id": 1})
|
||||||
args=["/tmp/test.txt"],
|
|
||||||
kwargs={"file_id": 1}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verify task ID was generated
|
# Verify task ID was generated
|
||||||
assert result.id is not None
|
assert result.id is not None
|
||||||
@@ -156,8 +163,10 @@ class TestEndToEndWithRedis:
|
|||||||
test_file.write_text(f"Test file {i}")
|
test_file.write_text(f"Test file {i}")
|
||||||
files.append(str(test_file))
|
files.append(str(test_file))
|
||||||
|
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = webdav_container["url"] + "/"
|
mock_settings.webdav_url = webdav_container["url"] + "/"
|
||||||
mock_settings.webdav_username = webdav_container["username"]
|
mock_settings.webdav_username = webdav_container["username"]
|
||||||
@@ -169,10 +178,7 @@ class TestEndToEndWithRedis:
|
|||||||
# Create folder on WebDAV server
|
# Create folder on WebDAV server
|
||||||
folder_url = f"{webdav_container['url']}/parallel-test"
|
folder_url = f"{webdav_container['url']}/parallel-test"
|
||||||
requests.request(
|
requests.request(
|
||||||
"MKCOL",
|
"MKCOL", folder_url, auth=(webdav_container["username"], webdav_container["password"]), timeout=5
|
||||||
folder_url,
|
|
||||||
auth=(webdav_container["username"], webdav_container["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Queue all tasks
|
# Queue all tasks
|
||||||
@@ -202,9 +208,7 @@ class TestEndToEndWithRedis:
|
|||||||
filename = os.path.basename(file_path)
|
filename = os.path.basename(file_path)
|
||||||
file_url = f"{webdav_container['url']}/parallel-test/{filename}"
|
file_url = f"{webdav_container['url']}/parallel-test/{filename}"
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
file_url,
|
file_url, auth=(webdav_container["username"], webdav_container["password"]), timeout=5
|
||||||
auth=(webdav_container["username"], webdav_container["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
@@ -222,13 +226,15 @@ class TestEndToEndWithRedis:
|
|||||||
"""
|
"""
|
||||||
from app.tasks.upload_to_webdav import upload_to_webdav
|
from app.tasks.upload_to_webdav import upload_to_webdav
|
||||||
|
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"), \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put:
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "http://test.com/"
|
mock_settings.webdav_url = "http://test.com/"
|
||||||
mock_settings.webdav_username = "user"
|
mock_settings.webdav_username = "user"
|
||||||
mock_settings.webdav_password = "pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = ""
|
mock_settings.webdav_folder = ""
|
||||||
mock_settings.webdav_verify_ssl = False
|
mock_settings.webdav_verify_ssl = False
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -280,6 +286,7 @@ class TestFullInfrastructure:
|
|||||||
# Check Redis
|
# Check Redis
|
||||||
assert infra["redis"]["url"] is not None
|
assert infra["redis"]["url"] is not None
|
||||||
import redis
|
import redis
|
||||||
|
|
||||||
r = redis.from_url(infra["redis"]["url"])
|
r = redis.from_url(infra["redis"]["url"])
|
||||||
assert r.ping()
|
assert r.ping()
|
||||||
|
|
||||||
@@ -298,6 +305,10 @@ class TestFullInfrastructure:
|
|||||||
# Check MinIO
|
# Check MinIO
|
||||||
assert infra["minio"]["access_key"] is not None
|
assert infra["minio"]["access_key"] is not None
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not _has_psycopg2,
|
||||||
|
reason="psycopg2 not installed",
|
||||||
|
)
|
||||||
def test_database_with_real_postgres(self, postgres_container, db_session_real):
|
def test_database_with_real_postgres(self, postgres_container, db_session_real):
|
||||||
"""
|
"""
|
||||||
Test database operations with real PostgreSQL instead of SQLite.
|
Test database operations with real PostgreSQL instead of SQLite.
|
||||||
@@ -340,8 +351,10 @@ class TestFullInfrastructure:
|
|||||||
|
|
||||||
infra = full_infrastructure
|
infra = full_infrastructure
|
||||||
|
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = infra["webdav"]["url"] + "/"
|
mock_settings.webdav_url = infra["webdav"]["url"] + "/"
|
||||||
mock_settings.webdav_username = infra["webdav"]["username"]
|
mock_settings.webdav_username = infra["webdav"]["username"]
|
||||||
@@ -369,9 +382,7 @@ class TestFullInfrastructure:
|
|||||||
filename = os.path.basename(sample_text_file)
|
filename = os.path.basename(sample_text_file)
|
||||||
file_url = f"{infra['webdav']['url']}/{filename}"
|
file_url = f"{infra['webdav']['url']}/{filename}"
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
file_url,
|
file_url, auth=(infra["webdav"]["username"], infra["webdav"]["password"]), timeout=5
|
||||||
auth=(infra["webdav"]["username"], infra["webdav"]["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
@@ -395,9 +406,7 @@ class TestFullInfrastructure:
|
|||||||
with open(html_file, "rb") as f:
|
with open(html_file, "rb") as f:
|
||||||
files = {"files": f}
|
files = {"files": f}
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{gotenberg_container['url']}/forms/chromium/convert/html",
|
f"{gotenberg_container['url']}/forms/chromium/convert/html", files=files, timeout=30
|
||||||
files=files,
|
|
||||||
timeout=30
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -443,8 +452,7 @@ class TestFullInfrastructure:
|
|||||||
download_path = os.path.join(os.path.dirname(sample_text_file), "downloaded.txt")
|
download_path = os.path.join(os.path.dirname(sample_text_file), "downloaded.txt")
|
||||||
s3_client.download_file(bucket_name, filename, download_path)
|
s3_client.download_file(bucket_name, filename, download_path)
|
||||||
|
|
||||||
with open(sample_text_file, "rb") as original, \
|
with open(sample_text_file, "rb") as original, open(download_path, "rb") as downloaded:
|
||||||
open(download_path, "rb") as downloaded:
|
|
||||||
assert original.read() == downloaded.read()
|
assert original.read() == downloaded.read()
|
||||||
|
|
||||||
def test_sftp_upload(self, sftp_container, sample_text_file):
|
def test_sftp_upload(self, sftp_container, sample_text_file):
|
||||||
@@ -483,8 +491,7 @@ class TestFullInfrastructure:
|
|||||||
download_path = os.path.join(os.path.dirname(sample_text_file), "sftp_downloaded.txt")
|
download_path = os.path.join(os.path.dirname(sample_text_file), "sftp_downloaded.txt")
|
||||||
sftp.get(remote_path, download_path)
|
sftp.get(remote_path, download_path)
|
||||||
|
|
||||||
with open(sample_text_file, "rb") as original, \
|
with open(sample_text_file, "rb") as original, open(download_path, "rb") as downloaded:
|
||||||
open(download_path, "rb") as downloaded:
|
|
||||||
assert original.read() == downloaded.read()
|
assert original.read() == downloaded.read()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
@@ -543,8 +550,10 @@ class TestProductionLikeScenarios:
|
|||||||
# Step 2: Queue upload task
|
# Step 2: Queue upload task
|
||||||
infra = full_infrastructure
|
infra = full_infrastructure
|
||||||
|
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = infra["webdav"]["url"] + "/"
|
mock_settings.webdav_url = infra["webdav"]["url"] + "/"
|
||||||
mock_settings.webdav_username = infra["webdav"]["username"]
|
mock_settings.webdav_username = infra["webdav"]["username"]
|
||||||
@@ -556,10 +565,7 @@ class TestProductionLikeScenarios:
|
|||||||
# Create folder
|
# Create folder
|
||||||
folder_url = f"{infra['webdav']['url']}/processed"
|
folder_url = f"{infra['webdav']['url']}/processed"
|
||||||
requests.request(
|
requests.request(
|
||||||
"MKCOL",
|
"MKCOL", folder_url, auth=(infra["webdav"]["username"], infra["webdav"]["password"]), timeout=5
|
||||||
folder_url,
|
|
||||||
auth=(infra["webdav"]["username"], infra["webdav"]["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 3: Queue upload
|
# Step 3: Queue upload
|
||||||
@@ -580,9 +586,7 @@ class TestProductionLikeScenarios:
|
|||||||
# Step 6: Verify file on WebDAV
|
# Step 6: Verify file on WebDAV
|
||||||
file_url = f"{infra['webdav']['url']}/processed/invoice.pdf"
|
file_url = f"{infra['webdav']['url']}/processed/invoice.pdf"
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
file_url,
|
file_url, auth=(infra["webdav"]["username"], infra["webdav"]["password"]), timeout=5
|
||||||
auth=(infra["webdav"]["username"], infra["webdav"]["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.content == test_doc.read_bytes()
|
assert response.content == test_doc.read_bytes()
|
||||||
|
|||||||
@@ -0,0 +1,822 @@
|
|||||||
|
"""
|
||||||
|
Integration tests for external API services using real credentials.
|
||||||
|
|
||||||
|
These tests verify that external API integrations work correctly when real
|
||||||
|
credentials are provided via environment variables (e.g., GitHub Actions secrets).
|
||||||
|
|
||||||
|
Each test is guarded by ``pytest.mark.skipif`` so it is skipped automatically
|
||||||
|
when the required environment variables are absent or still set to placeholder
|
||||||
|
values. All tests carry the ``@pytest.mark.requires_external`` marker so they
|
||||||
|
can be run (or excluded) with::
|
||||||
|
|
||||||
|
pytest -m requires_external # run only external tests
|
||||||
|
pytest -m "not requires_external" # skip external tests
|
||||||
|
|
||||||
|
**Pipeline coverage:**
|
||||||
|
|
||||||
|
The tests exercise real end-to-end flows wherever credentials allow:
|
||||||
|
|
||||||
|
- *OpenAI*: key validation **and** metadata extraction via chat completion.
|
||||||
|
- *Azure Document Intelligence*: admin connectivity **and** OCR of a generated PDF.
|
||||||
|
- *S3*: bucket access, file upload, download verification, and cleanup.
|
||||||
|
- *Dropbox*: token refresh, file upload, download verification, and cleanup.
|
||||||
|
- *OneDrive*: token refresh, file upload, download verification, and cleanup.
|
||||||
|
- *Authentik/OIDC*: discovery endpoint and credential consistency.
|
||||||
|
|
||||||
|
Each pipeline test dynamically generates a unique PDF with ``fpdf2`` so every
|
||||||
|
run operates on fresh data. Uploaded test files are cleaned up in ``finally``
|
||||||
|
blocks to avoid polluting external storage.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import uuid
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.conftest import has_real_env
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test-PDF generator helper
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
_TEST_PREFIX = "docuelevate_test_"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_test_pdf(
|
||||||
|
content: Optional[str] = None,
|
||||||
|
filename_prefix: str = _TEST_PREFIX,
|
||||||
|
) -> str:
|
||||||
|
"""Generate a unique test PDF with embedded text.
|
||||||
|
|
||||||
|
Creates a one-page PDF containing *content* (or a random invoice stub)
|
||||||
|
and returns the path to the temporary file. The caller is responsible
|
||||||
|
for deleting the file when done.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Optional text to embed. When ``None`` a realistic
|
||||||
|
invoice-style document is generated.
|
||||||
|
filename_prefix: Prefix for the temp filename.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Absolute path to the generated PDF file.
|
||||||
|
"""
|
||||||
|
from fpdf import FPDF
|
||||||
|
|
||||||
|
unique_id = uuid.uuid4().hex[:8]
|
||||||
|
|
||||||
|
if content is None:
|
||||||
|
content = (
|
||||||
|
f"Invoice #{unique_id}\n"
|
||||||
|
f"Date: 2024-06-15\n"
|
||||||
|
f"From: Acme Integration Testing GmbH\n"
|
||||||
|
f"To: DocuElevate QA Department\n"
|
||||||
|
f"Amount: EUR 1,234.56\n\n"
|
||||||
|
f"Description: Annual subscription renewal for cloud document\n"
|
||||||
|
f"processing services. Reference: REF-{unique_id}.\n\n"
|
||||||
|
f"Payment terms: Net 30 days.\n"
|
||||||
|
f"Bank: Deutsche Bank, IBAN: DE89 3704 0044 0532 0130 00\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
pdf = FPDF()
|
||||||
|
pdf.add_page()
|
||||||
|
pdf.set_font("Helvetica", size=11)
|
||||||
|
pdf.multi_cell(0, 7, text=content)
|
||||||
|
|
||||||
|
fd, path = tempfile.mkstemp(prefix=filename_prefix, suffix=".pdf")
|
||||||
|
os.close(fd)
|
||||||
|
pdf.output(path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# OpenAI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@pytest.mark.requires_external
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not has_real_env("OPENAI_API_KEY"),
|
||||||
|
reason="Real OPENAI_API_KEY not available",
|
||||||
|
)
|
||||||
|
class TestOpenAIIntegration:
|
||||||
|
"""Verify OpenAI API connectivity and metadata extraction with real credentials."""
|
||||||
|
|
||||||
|
def test_openai_api_key_is_valid(self, original_env: dict) -> None:
|
||||||
|
"""Validate that the configured OpenAI API key can list models."""
|
||||||
|
import openai
|
||||||
|
|
||||||
|
api_key = original_env["OPENAI_API_KEY"]
|
||||||
|
base_url = original_env.get("OPENAI_BASE_URL") or "https://api.openai.com/v1"
|
||||||
|
|
||||||
|
oai = openai.OpenAI(api_key=api_key, base_url=base_url)
|
||||||
|
models = oai.models.list()
|
||||||
|
|
||||||
|
assert hasattr(models, "data"), "Expected models response to have 'data' attribute"
|
||||||
|
assert len(models.data) > 0, "Expected at least one model to be available"
|
||||||
|
|
||||||
|
def test_openai_test_endpoint_with_real_key(self, client, original_env: dict, monkeypatch) -> None:
|
||||||
|
"""Test the /api/openai/test endpoint returns success with a real API key."""
|
||||||
|
monkeypatch.setattr("app.config.settings.openai_api_key", original_env["OPENAI_API_KEY"])
|
||||||
|
if original_env.get("OPENAI_BASE_URL"):
|
||||||
|
monkeypatch.setattr("app.config.settings.openai_base_url", original_env["OPENAI_BASE_URL"])
|
||||||
|
|
||||||
|
response = client.get("/api/openai/test")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "success", f"OpenAI test endpoint failed: {data.get('message')}"
|
||||||
|
assert data.get("models_available", 0) > 0
|
||||||
|
|
||||||
|
def test_openai_metadata_extraction(self, original_env: dict) -> None:
|
||||||
|
"""End-to-end: send document text to OpenAI and receive structured metadata."""
|
||||||
|
import openai
|
||||||
|
|
||||||
|
api_key = original_env["OPENAI_API_KEY"]
|
||||||
|
base_url = original_env.get("OPENAI_BASE_URL") or "https://api.openai.com/v1"
|
||||||
|
|
||||||
|
# Use the same prompt structure as extract_metadata_with_gpt task
|
||||||
|
unique_id = uuid.uuid4().hex[:8]
|
||||||
|
sample_text = (
|
||||||
|
f"Invoice #{unique_id}\n"
|
||||||
|
f"Date: 2024-06-15\n"
|
||||||
|
f"From: Acme Integration Testing GmbH\n"
|
||||||
|
f"To: DocuElevate QA Department\n"
|
||||||
|
f"Amount: EUR 1,234.56\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = (
|
||||||
|
"You are a specialized document analyzer. Analyze the given text and return a JSON object with:\n"
|
||||||
|
'- "document_type": precise classification (e.g., Invoice, Contract)\n'
|
||||||
|
'- "language": ISO 639-1 code\n'
|
||||||
|
'- "tags": list of up to 4 keywords\n'
|
||||||
|
'- "absender": sender name\n'
|
||||||
|
'- "empfaenger": recipient name\n\n'
|
||||||
|
f"Text:\n{sample_text}\n\n"
|
||||||
|
"Return only valid JSON."
|
||||||
|
)
|
||||||
|
|
||||||
|
oai = openai.OpenAI(api_key=api_key, base_url=base_url)
|
||||||
|
completion = oai.chat.completions.create(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "You are an intelligent document classifier."},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
temperature=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
content = completion.choices[0].message.content
|
||||||
|
assert content, "OpenAI returned empty content"
|
||||||
|
|
||||||
|
# Extract JSON from response (may be wrapped in markdown fences)
|
||||||
|
import re
|
||||||
|
|
||||||
|
json_match = re.search(r"\{.*\}", content, re.DOTALL)
|
||||||
|
assert json_match, f"No JSON found in OpenAI response: {content[:200]}"
|
||||||
|
|
||||||
|
metadata = json.loads(json_match.group())
|
||||||
|
assert "document_type" in metadata, "Missing document_type in extracted metadata"
|
||||||
|
assert "language" in metadata, "Missing language in extracted metadata"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Azure Document Intelligence
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@pytest.mark.requires_external
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not has_real_env("AZURE_AI_KEY", "AZURE_ENDPOINT"),
|
||||||
|
reason="Real AZURE_AI_KEY and AZURE_ENDPOINT not available",
|
||||||
|
)
|
||||||
|
class TestAzureDocumentIntelligenceIntegration:
|
||||||
|
"""Verify Azure Document Intelligence connectivity and OCR with real credentials."""
|
||||||
|
|
||||||
|
def test_azure_admin_client_connects(self, original_env: dict) -> None:
|
||||||
|
"""Validate that the Azure admin client can list operations."""
|
||||||
|
from azure.ai.documentintelligence import DocumentIntelligenceAdministrationClient
|
||||||
|
from azure.core.credentials import AzureKeyCredential
|
||||||
|
|
||||||
|
admin_client = DocumentIntelligenceAdministrationClient(
|
||||||
|
endpoint=original_env["AZURE_ENDPOINT"],
|
||||||
|
credential=AzureKeyCredential(original_env["AZURE_AI_KEY"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
operations = list(admin_client.list_operations())
|
||||||
|
assert isinstance(operations, list)
|
||||||
|
|
||||||
|
def test_azure_test_endpoint_with_real_credentials(self, client, original_env: dict, monkeypatch) -> None:
|
||||||
|
"""Test the /api/azure/test endpoint returns success with real credentials."""
|
||||||
|
monkeypatch.setattr("app.config.settings.azure_ai_key", original_env["AZURE_AI_KEY"])
|
||||||
|
monkeypatch.setattr("app.config.settings.azure_endpoint", original_env["AZURE_ENDPOINT"])
|
||||||
|
if original_env.get("AZURE_REGION"):
|
||||||
|
monkeypatch.setattr("app.config.settings.azure_region", original_env["AZURE_REGION"])
|
||||||
|
|
||||||
|
response = client.get("/api/azure/test")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "success", f"Azure test endpoint failed: {data.get('message')}"
|
||||||
|
|
||||||
|
def test_azure_ocr_on_generated_pdf(self, original_env: dict) -> None:
|
||||||
|
"""End-to-end: send a generated PDF to Azure and receive OCR text back."""
|
||||||
|
from azure.ai.documentintelligence import DocumentIntelligenceClient
|
||||||
|
from azure.ai.documentintelligence.models import AnalyzeOutputOption
|
||||||
|
from azure.core.credentials import AzureKeyCredential
|
||||||
|
|
||||||
|
pdf_path = generate_test_pdf()
|
||||||
|
try:
|
||||||
|
doc_client = DocumentIntelligenceClient(
|
||||||
|
endpoint=original_env["AZURE_ENDPOINT"],
|
||||||
|
credential=AzureKeyCredential(original_env["AZURE_AI_KEY"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(pdf_path, "rb") as f:
|
||||||
|
poller = doc_client.begin_analyze_document(
|
||||||
|
"prebuilt-read",
|
||||||
|
body=f,
|
||||||
|
output=[AnalyzeOutputOption.PDF],
|
||||||
|
)
|
||||||
|
result = poller.result()
|
||||||
|
|
||||||
|
assert result.content, "Azure OCR returned no content"
|
||||||
|
assert len(result.content) > 10, f"OCR text too short: {result.content[:50]}"
|
||||||
|
|
||||||
|
# Verify the generated text is recognizable
|
||||||
|
assert (
|
||||||
|
"Acme" in result.content or "Invoice" in result.content
|
||||||
|
), f"OCR text does not contain expected keywords: {result.content[:200]}"
|
||||||
|
|
||||||
|
# Retrieve the searchable PDF output
|
||||||
|
operation_id = poller.details["operation_id"]
|
||||||
|
pdf_response = doc_client.get_analyze_result_pdf(
|
||||||
|
model_id=result.model_id,
|
||||||
|
result_id=operation_id,
|
||||||
|
)
|
||||||
|
searchable_bytes = b"".join(pdf_response)
|
||||||
|
assert len(searchable_bytes) > 0, "Searchable PDF output is empty"
|
||||||
|
assert searchable_bytes[:5] == b"%PDF-", "Output is not a valid PDF"
|
||||||
|
finally:
|
||||||
|
os.unlink(pdf_path)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# AWS S3 – full upload/download/delete pipeline
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@pytest.mark.requires_external
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not has_real_env("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "S3_BUCKET_NAME"),
|
||||||
|
reason="Real AWS credentials and S3_BUCKET_NAME not available",
|
||||||
|
)
|
||||||
|
class TestS3Integration:
|
||||||
|
"""Verify AWS S3 connectivity and upload/download pipeline with real credentials."""
|
||||||
|
|
||||||
|
def test_s3_bucket_accessible(self, original_env: dict) -> None:
|
||||||
|
"""Validate that the S3 bucket exists and credentials are accepted."""
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
region_name=original_env.get("AWS_REGION", "us-east-1"),
|
||||||
|
aws_access_key_id=original_env["AWS_ACCESS_KEY_ID"],
|
||||||
|
aws_secret_access_key=original_env["AWS_SECRET_ACCESS_KEY"],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = s3_client.head_bucket(Bucket=original_env["S3_BUCKET_NAME"])
|
||||||
|
assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
|
||||||
|
|
||||||
|
def test_s3_upload_download_delete(self, original_env: dict) -> None:
|
||||||
|
"""End-to-end: upload a generated PDF to S3, download and verify, then delete."""
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
pdf_path = generate_test_pdf()
|
||||||
|
s3_key = None
|
||||||
|
try:
|
||||||
|
s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
region_name=original_env.get("AWS_REGION", "us-east-1"),
|
||||||
|
aws_access_key_id=original_env["AWS_ACCESS_KEY_ID"],
|
||||||
|
aws_secret_access_key=original_env["AWS_SECRET_ACCESS_KEY"],
|
||||||
|
)
|
||||||
|
bucket = original_env["S3_BUCKET_NAME"]
|
||||||
|
prefix = original_env.get("S3_FOLDER_PREFIX", "")
|
||||||
|
if prefix and not prefix.endswith("/"):
|
||||||
|
prefix += "/"
|
||||||
|
s3_key = f"{prefix}{_TEST_PREFIX}{uuid.uuid4().hex[:8]}.pdf"
|
||||||
|
|
||||||
|
# Upload
|
||||||
|
s3_client.upload_file(pdf_path, bucket, s3_key)
|
||||||
|
|
||||||
|
# Download and verify
|
||||||
|
download_path = pdf_path + ".downloaded"
|
||||||
|
s3_client.download_file(bucket, s3_key, download_path)
|
||||||
|
|
||||||
|
with open(pdf_path, "rb") as orig, open(download_path, "rb") as dl:
|
||||||
|
assert orig.read() == dl.read(), "Downloaded file does not match uploaded file"
|
||||||
|
|
||||||
|
os.unlink(download_path)
|
||||||
|
finally:
|
||||||
|
os.unlink(pdf_path)
|
||||||
|
# Cleanup: delete the test object from S3
|
||||||
|
if s3_key:
|
||||||
|
try:
|
||||||
|
s3_client.delete_object(Bucket=bucket, Key=s3_key)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to clean up S3 test object %s: %s", s3_key, exc)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Dropbox – full upload/download/delete pipeline
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@pytest.mark.requires_external
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not has_real_env("DROPBOX_APP_KEY", "DROPBOX_APP_SECRET", "DROPBOX_REFRESH_TOKEN"),
|
||||||
|
reason="Real Dropbox credentials not available",
|
||||||
|
)
|
||||||
|
class TestDropboxIntegration:
|
||||||
|
"""Verify Dropbox token validity and upload/download pipeline with real credentials."""
|
||||||
|
|
||||||
|
def test_dropbox_token_refresh_and_account_info(self, original_env: dict) -> None:
|
||||||
|
"""Validate token refresh and account info retrieval in one go."""
|
||||||
|
import dropbox as dbx_lib
|
||||||
|
|
||||||
|
dbx = dbx_lib.Dropbox(
|
||||||
|
app_key=original_env["DROPBOX_APP_KEY"],
|
||||||
|
app_secret=original_env["DROPBOX_APP_SECRET"],
|
||||||
|
oauth2_refresh_token=original_env["DROPBOX_REFRESH_TOKEN"],
|
||||||
|
)
|
||||||
|
account = dbx.users_get_current_account()
|
||||||
|
assert account.email, "Dropbox account missing email"
|
||||||
|
|
||||||
|
def test_dropbox_upload_download_delete(self, original_env: dict) -> None:
|
||||||
|
"""End-to-end: upload a generated PDF to Dropbox, download it, then delete it."""
|
||||||
|
import dropbox as dbx_lib
|
||||||
|
|
||||||
|
pdf_path = generate_test_pdf()
|
||||||
|
remote_path = f"/{_TEST_PREFIX}{uuid.uuid4().hex[:8]}.pdf"
|
||||||
|
dbx = None
|
||||||
|
try:
|
||||||
|
dbx = dbx_lib.Dropbox(
|
||||||
|
app_key=original_env["DROPBOX_APP_KEY"],
|
||||||
|
app_secret=original_env["DROPBOX_APP_SECRET"],
|
||||||
|
oauth2_refresh_token=original_env["DROPBOX_REFRESH_TOKEN"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upload
|
||||||
|
with open(pdf_path, "rb") as f:
|
||||||
|
dbx.files_upload(
|
||||||
|
f.read(),
|
||||||
|
remote_path,
|
||||||
|
mode=dbx_lib.files.WriteMode.overwrite,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Download and verify
|
||||||
|
_, response = dbx.files_download(remote_path)
|
||||||
|
downloaded = response.content
|
||||||
|
with open(pdf_path, "rb") as f:
|
||||||
|
assert f.read() == downloaded, "Downloaded Dropbox file does not match uploaded file"
|
||||||
|
finally:
|
||||||
|
os.unlink(pdf_path)
|
||||||
|
# Cleanup
|
||||||
|
if dbx:
|
||||||
|
try:
|
||||||
|
dbx.files_delete_v2(remote_path)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to clean up Dropbox test file %s: %s", remote_path, exc)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# OneDrive – full upload/download/delete pipeline
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@pytest.mark.requires_external
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not has_real_env("ONEDRIVE_CLIENT_ID", "ONEDRIVE_CLIENT_SECRET", "ONEDRIVE_REFRESH_TOKEN"),
|
||||||
|
reason="Real OneDrive credentials not available",
|
||||||
|
)
|
||||||
|
class TestOneDriveIntegration:
|
||||||
|
"""Verify OneDrive token validity and upload/download pipeline with real credentials."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_access_token(env: dict) -> str:
|
||||||
|
"""Obtain a fresh OneDrive access token via MSAL."""
|
||||||
|
import msal
|
||||||
|
|
||||||
|
tenant = env.get("ONEDRIVE_TENANT_ID") or "common"
|
||||||
|
app = msal.ConfidentialClientApplication(
|
||||||
|
client_id=env["ONEDRIVE_CLIENT_ID"],
|
||||||
|
client_credential=env["ONEDRIVE_CLIENT_SECRET"],
|
||||||
|
authority=f"https://login.microsoftonline.com/{tenant}",
|
||||||
|
)
|
||||||
|
result = app.acquire_token_by_refresh_token(
|
||||||
|
refresh_token=env["ONEDRIVE_REFRESH_TOKEN"],
|
||||||
|
scopes=["https://graph.microsoft.com/.default"],
|
||||||
|
)
|
||||||
|
assert "access_token" in result, f"OneDrive token acquisition failed: {result.get('error_description')}"
|
||||||
|
return result["access_token"]
|
||||||
|
|
||||||
|
def test_onedrive_token_refresh_and_user_info(self, original_env: dict) -> None:
|
||||||
|
"""Validate token refresh and user info retrieval."""
|
||||||
|
import requests
|
||||||
|
|
||||||
|
token = self._get_access_token(original_env)
|
||||||
|
resp = requests.get(
|
||||||
|
"https://graph.microsoft.com/v1.0/me",
|
||||||
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200, f"OneDrive user info failed: {resp.text}"
|
||||||
|
|
||||||
|
def test_onedrive_upload_download_delete(self, original_env: dict) -> None:
|
||||||
|
"""End-to-end: upload a generated PDF to OneDrive, download it, then delete it."""
|
||||||
|
import requests
|
||||||
|
|
||||||
|
pdf_path = generate_test_pdf()
|
||||||
|
filename = f"{_TEST_PREFIX}{uuid.uuid4().hex[:8]}.pdf"
|
||||||
|
folder = original_env.get("ONEDRIVE_FOLDER_PATH", "").strip("/")
|
||||||
|
item_id = None
|
||||||
|
token = None
|
||||||
|
try:
|
||||||
|
token = self._get_access_token(original_env)
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
# Upload (simple upload for small files)
|
||||||
|
if folder:
|
||||||
|
upload_url = f"https://graph.microsoft.com/v1.0/me/drive/root:/{folder}/{filename}:/content"
|
||||||
|
else:
|
||||||
|
upload_url = f"https://graph.microsoft.com/v1.0/me/drive/root:/{filename}:/content"
|
||||||
|
|
||||||
|
with open(pdf_path, "rb") as f:
|
||||||
|
upload_resp = requests.put(
|
||||||
|
upload_url,
|
||||||
|
headers={**headers, "Content-Type": "application/pdf"},
|
||||||
|
data=f.read(),
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
assert upload_resp.status_code in (200, 201), f"OneDrive upload failed: {upload_resp.text}"
|
||||||
|
item_id = upload_resp.json().get("id")
|
||||||
|
|
||||||
|
# Download and verify
|
||||||
|
download_url = f"https://graph.microsoft.com/v1.0/me/drive/items/{item_id}/content"
|
||||||
|
dl_resp = requests.get(download_url, headers=headers, timeout=60)
|
||||||
|
assert dl_resp.status_code == 200, f"OneDrive download failed: {dl_resp.status_code}"
|
||||||
|
|
||||||
|
with open(pdf_path, "rb") as f:
|
||||||
|
assert f.read() == dl_resp.content, "Downloaded OneDrive file does not match uploaded file"
|
||||||
|
finally:
|
||||||
|
os.unlink(pdf_path)
|
||||||
|
# Cleanup
|
||||||
|
if item_id and token:
|
||||||
|
try:
|
||||||
|
requests.delete(
|
||||||
|
f"https://graph.microsoft.com/v1.0/me/drive/items/{item_id}",
|
||||||
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to clean up OneDrive test file %s: %s", filename, exc)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Authentik / OpenID Connect – discovery endpoint validation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@pytest.mark.requires_external
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not has_real_env("AUTHENTIK_CONFIG_URL"),
|
||||||
|
reason="Real AUTHENTIK_CONFIG_URL not available",
|
||||||
|
)
|
||||||
|
class TestAuthentikIntegration:
|
||||||
|
"""Verify Authentik / OpenID Connect discovery endpoint is reachable."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_oidc_discovery_endpoint(original_env: dict) -> None:
|
||||||
|
"""Validate that the OIDC discovery URL returns a valid JSON document."""
|
||||||
|
import requests
|
||||||
|
|
||||||
|
config_url = original_env["AUTHENTIK_CONFIG_URL"]
|
||||||
|
response = requests.get(config_url, timeout=30)
|
||||||
|
|
||||||
|
assert response.status_code == 200, f"OIDC discovery failed: {response.status_code}"
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
assert "issuer" in data, "OIDC response missing 'issuer'"
|
||||||
|
assert "authorization_endpoint" in data, "OIDC response missing 'authorization_endpoint'"
|
||||||
|
assert "token_endpoint" in data, "OIDC response missing 'token_endpoint'"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_authentik_client_credentials_present(original_env: dict) -> None:
|
||||||
|
"""Validate that Authentik client credentials are configured alongside the config URL."""
|
||||||
|
client_id = original_env.get("AUTHENTIK_CLIENT_ID")
|
||||||
|
client_secret = original_env.get("AUTHENTIK_CLIENT_SECRET")
|
||||||
|
|
||||||
|
if client_id and client_secret:
|
||||||
|
assert len(client_id) > 0, "AUTHENTIK_CLIENT_ID should not be empty"
|
||||||
|
assert len(client_secret) > 0, "AUTHENTIK_CLIENT_SECRET should not be empty"
|
||||||
|
else:
|
||||||
|
pytest.skip("AUTHENTIK_CLIENT_ID and/or AUTHENTIK_CLIENT_SECRET not set")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Full pipeline: Azure OCR → OpenAI metadata extraction (end-to-end)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@pytest.mark.requires_external
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not has_real_env("AZURE_AI_KEY", "AZURE_ENDPOINT", "OPENAI_API_KEY"),
|
||||||
|
reason="Real Azure + OpenAI credentials required for full pipeline test",
|
||||||
|
)
|
||||||
|
class TestFullOCRMetadataPipeline:
|
||||||
|
"""End-to-end pipeline: generate PDF → Azure OCR → OpenAI metadata extraction.
|
||||||
|
|
||||||
|
This replicates the core DocuElevate processing flow without requiring
|
||||||
|
Celery or Redis, by calling the service APIs directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_ocr_then_metadata_extraction(original_env: dict) -> None:
|
||||||
|
"""Generate a PDF, OCR it with Azure, then extract metadata with OpenAI."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
import openai
|
||||||
|
from azure.ai.documentintelligence import DocumentIntelligenceClient
|
||||||
|
from azure.ai.documentintelligence.models import AnalyzeOutputOption
|
||||||
|
from azure.core.credentials import AzureKeyCredential
|
||||||
|
|
||||||
|
pdf_path = generate_test_pdf()
|
||||||
|
try:
|
||||||
|
# --- Step 1: Azure OCR ---
|
||||||
|
doc_client = DocumentIntelligenceClient(
|
||||||
|
endpoint=original_env["AZURE_ENDPOINT"],
|
||||||
|
credential=AzureKeyCredential(original_env["AZURE_AI_KEY"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(pdf_path, "rb") as f:
|
||||||
|
poller = doc_client.begin_analyze_document(
|
||||||
|
"prebuilt-read",
|
||||||
|
body=f,
|
||||||
|
output=[AnalyzeOutputOption.PDF],
|
||||||
|
)
|
||||||
|
result = poller.result()
|
||||||
|
extracted_text = result.content
|
||||||
|
assert extracted_text and len(extracted_text) > 10, "OCR produced insufficient text"
|
||||||
|
|
||||||
|
# --- Step 2: OpenAI metadata extraction ---
|
||||||
|
api_key = original_env["OPENAI_API_KEY"]
|
||||||
|
base_url = original_env.get("OPENAI_BASE_URL") or "https://api.openai.com/v1"
|
||||||
|
oai = openai.OpenAI(api_key=api_key, base_url=base_url)
|
||||||
|
|
||||||
|
prompt = (
|
||||||
|
"You are a specialized document analyzer. Analyze the following OCR-extracted text "
|
||||||
|
"and return a JSON object with these fields:\n"
|
||||||
|
'- "document_type": classification (e.g. Invoice, Contract, Letter)\n'
|
||||||
|
'- "language": ISO 639-1 code\n'
|
||||||
|
'- "absender": sender\n'
|
||||||
|
'- "empfaenger": recipient\n'
|
||||||
|
'- "tags": up to 4 keywords\n'
|
||||||
|
'- "confidence_score": 0-100\n\n'
|
||||||
|
f"OCR text:\n{extracted_text}\n\n"
|
||||||
|
"Return only valid JSON."
|
||||||
|
)
|
||||||
|
|
||||||
|
completion = oai.chat.completions.create(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "You are an intelligent document classifier."},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
temperature=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
content = completion.choices[0].message.content
|
||||||
|
assert content, "OpenAI returned empty response"
|
||||||
|
|
||||||
|
json_match = re.search(r"\{.*\}", content, re.DOTALL)
|
||||||
|
assert json_match, f"No JSON found in response: {content[:200]}"
|
||||||
|
|
||||||
|
metadata = json.loads(json_match.group())
|
||||||
|
|
||||||
|
# Validate key metadata fields
|
||||||
|
assert "document_type" in metadata, "Missing document_type"
|
||||||
|
assert "language" in metadata, "Missing language"
|
||||||
|
assert "absender" in metadata, "Missing absender"
|
||||||
|
assert "empfaenger" in metadata, "Missing empfaenger"
|
||||||
|
|
||||||
|
# The generated invoice should be classified reasonably
|
||||||
|
doc_type = metadata["document_type"].lower()
|
||||||
|
assert any(
|
||||||
|
kw in doc_type for kw in ("invoice", "rechnung", "bill")
|
||||||
|
), f"Unexpected document_type: {metadata['document_type']}"
|
||||||
|
finally:
|
||||||
|
os.unlink(pdf_path)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Configuration: verify that Settings correctly loads external env vars
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@pytest.mark.unit
|
||||||
|
class TestExternalEnvVarConfiguration:
|
||||||
|
"""Verify that the Settings class correctly reads external API environment variables.
|
||||||
|
|
||||||
|
These tests do NOT call external APIs; they only validate that the configuration
|
||||||
|
layer correctly maps environment variables to Settings fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_settings_reads_openai_base_url() -> None:
|
||||||
|
"""Test that OPENAI_BASE_URL is correctly loaded into Settings."""
|
||||||
|
from app.config import Settings
|
||||||
|
|
||||||
|
custom_url = "https://custom-openai.example.com/v1"
|
||||||
|
config = Settings(
|
||||||
|
database_url="sqlite:///test.db",
|
||||||
|
redis_url="redis://localhost:6379",
|
||||||
|
openai_api_key="test",
|
||||||
|
openai_base_url=custom_url,
|
||||||
|
azure_ai_key="test",
|
||||||
|
azure_region="test",
|
||||||
|
azure_endpoint="https://test.example.com",
|
||||||
|
gotenberg_url="http://localhost:3000",
|
||||||
|
workdir="/tmp",
|
||||||
|
auth_enabled=False,
|
||||||
|
)
|
||||||
|
assert config.openai_base_url == custom_url
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_settings_reads_s3_configuration() -> None:
|
||||||
|
"""Test that S3-related settings are correctly loaded."""
|
||||||
|
from app.config import Settings
|
||||||
|
|
||||||
|
config = Settings(
|
||||||
|
database_url="sqlite:///test.db",
|
||||||
|
redis_url="redis://localhost:6379",
|
||||||
|
openai_api_key="test",
|
||||||
|
azure_ai_key="test",
|
||||||
|
azure_region="test",
|
||||||
|
azure_endpoint="https://test.example.com",
|
||||||
|
gotenberg_url="http://localhost:3000",
|
||||||
|
workdir="/tmp",
|
||||||
|
auth_enabled=False,
|
||||||
|
aws_access_key_id="AKIAEXAMPLE",
|
||||||
|
aws_secret_access_key="secretkey",
|
||||||
|
s3_bucket_name="my-bucket",
|
||||||
|
s3_folder_prefix="uploads/",
|
||||||
|
)
|
||||||
|
assert config.aws_access_key_id == "AKIAEXAMPLE"
|
||||||
|
assert config.aws_secret_access_key == "secretkey"
|
||||||
|
assert config.s3_bucket_name == "my-bucket"
|
||||||
|
assert config.s3_folder_prefix == "uploads/"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_settings_reads_onedrive_configuration() -> None:
|
||||||
|
"""Test that OneDrive-related settings are correctly loaded."""
|
||||||
|
from app.config import Settings
|
||||||
|
|
||||||
|
config = Settings(
|
||||||
|
database_url="sqlite:///test.db",
|
||||||
|
redis_url="redis://localhost:6379",
|
||||||
|
openai_api_key="test",
|
||||||
|
azure_ai_key="test",
|
||||||
|
azure_region="test",
|
||||||
|
azure_endpoint="https://test.example.com",
|
||||||
|
gotenberg_url="http://localhost:3000",
|
||||||
|
workdir="/tmp",
|
||||||
|
auth_enabled=False,
|
||||||
|
onedrive_client_id="client-123",
|
||||||
|
onedrive_client_secret="secret-456",
|
||||||
|
onedrive_tenant_id="tenant-789",
|
||||||
|
onedrive_refresh_token="refresh-abc",
|
||||||
|
onedrive_folder_path="Documents/Test",
|
||||||
|
)
|
||||||
|
assert config.onedrive_client_id == "client-123"
|
||||||
|
assert config.onedrive_client_secret == "secret-456"
|
||||||
|
assert config.onedrive_tenant_id == "tenant-789"
|
||||||
|
assert config.onedrive_refresh_token == "refresh-abc"
|
||||||
|
assert config.onedrive_folder_path == "Documents/Test"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_settings_reads_dropbox_configuration() -> None:
|
||||||
|
"""Test that Dropbox-related settings are correctly loaded."""
|
||||||
|
from app.config import Settings
|
||||||
|
|
||||||
|
config = Settings(
|
||||||
|
database_url="sqlite:///test.db",
|
||||||
|
redis_url="redis://localhost:6379",
|
||||||
|
openai_api_key="test",
|
||||||
|
azure_ai_key="test",
|
||||||
|
azure_region="test",
|
||||||
|
azure_endpoint="https://test.example.com",
|
||||||
|
gotenberg_url="http://localhost:3000",
|
||||||
|
workdir="/tmp",
|
||||||
|
auth_enabled=False,
|
||||||
|
dropbox_app_key="dbx-key",
|
||||||
|
dropbox_app_secret="dbx-secret",
|
||||||
|
dropbox_refresh_token="dbx-refresh",
|
||||||
|
)
|
||||||
|
assert config.dropbox_app_key == "dbx-key"
|
||||||
|
assert config.dropbox_app_secret == "dbx-secret"
|
||||||
|
assert config.dropbox_refresh_token == "dbx-refresh"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_settings_reads_authentik_configuration() -> None:
|
||||||
|
"""Test that Authentik/OIDC settings are correctly loaded."""
|
||||||
|
from app.config import Settings
|
||||||
|
|
||||||
|
config = Settings(
|
||||||
|
database_url="sqlite:///test.db",
|
||||||
|
redis_url="redis://localhost:6379",
|
||||||
|
openai_api_key="test",
|
||||||
|
azure_ai_key="test",
|
||||||
|
azure_region="test",
|
||||||
|
azure_endpoint="https://test.example.com",
|
||||||
|
gotenberg_url="http://localhost:3000",
|
||||||
|
workdir="/tmp",
|
||||||
|
auth_enabled=False,
|
||||||
|
authentik_client_id="auth-client",
|
||||||
|
authentik_client_secret="auth-secret",
|
||||||
|
authentik_config_url="https://auth.example.com/.well-known/openid-configuration",
|
||||||
|
)
|
||||||
|
assert config.authentik_client_id == "auth-client"
|
||||||
|
assert config.authentik_client_secret == "auth-secret"
|
||||||
|
assert config.authentik_config_url == "https://auth.example.com/.well-known/openid-configuration"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_settings_optional_services_default_to_none() -> None:
|
||||||
|
"""Test that optional external service settings default to None when not provided."""
|
||||||
|
from app.config import Settings
|
||||||
|
|
||||||
|
config = Settings(
|
||||||
|
database_url="sqlite:///test.db",
|
||||||
|
redis_url="redis://localhost:6379",
|
||||||
|
openai_api_key="test",
|
||||||
|
azure_ai_key="test",
|
||||||
|
azure_region="test",
|
||||||
|
azure_endpoint="https://test.example.com",
|
||||||
|
gotenberg_url="http://localhost:3000",
|
||||||
|
workdir="/tmp",
|
||||||
|
auth_enabled=False,
|
||||||
|
)
|
||||||
|
assert config.aws_access_key_id is None
|
||||||
|
assert config.aws_secret_access_key is None
|
||||||
|
assert config.s3_bucket_name is None
|
||||||
|
assert config.onedrive_client_id is None
|
||||||
|
assert config.onedrive_client_secret is None
|
||||||
|
assert config.onedrive_refresh_token is None
|
||||||
|
assert config.dropbox_app_key is None
|
||||||
|
assert config.dropbox_app_secret is None
|
||||||
|
assert config.dropbox_refresh_token is None
|
||||||
|
assert config.authentik_client_id is None
|
||||||
|
assert config.authentik_client_secret is None
|
||||||
|
assert config.authentik_config_url is None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test-PDF generator unit tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@pytest.mark.unit
|
||||||
|
class TestPdfGenerator:
|
||||||
|
"""Verify that the test PDF generator produces valid PDFs with extractable text."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_generate_default_pdf() -> None:
|
||||||
|
"""Test that generate_test_pdf creates a valid PDF with embedded text."""
|
||||||
|
import PyPDF2
|
||||||
|
|
||||||
|
path = generate_test_pdf()
|
||||||
|
try:
|
||||||
|
assert os.path.exists(path)
|
||||||
|
assert os.path.getsize(path) > 100
|
||||||
|
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
reader = PyPDF2.PdfReader(f)
|
||||||
|
assert len(reader.pages) >= 1
|
||||||
|
text = reader.pages[0].extract_text()
|
||||||
|
assert "Invoice" in text
|
||||||
|
assert "Acme" in text
|
||||||
|
finally:
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_generate_custom_content_pdf() -> None:
|
||||||
|
"""Test that generate_test_pdf accepts custom content."""
|
||||||
|
import PyPDF2
|
||||||
|
|
||||||
|
custom = "Custom test content for verification"
|
||||||
|
path = generate_test_pdf(content=custom)
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
reader = PyPDF2.PdfReader(f)
|
||||||
|
text = reader.pages[0].extract_text()
|
||||||
|
assert "Custom test content" in text
|
||||||
|
finally:
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_generated_pdfs_are_unique() -> None:
|
||||||
|
"""Test that consecutive calls produce different PDFs."""
|
||||||
|
path1 = generate_test_pdf()
|
||||||
|
path2 = generate_test_pdf()
|
||||||
|
try:
|
||||||
|
with open(path1, "rb") as f1, open(path2, "rb") as f2:
|
||||||
|
assert f1.read() != f2.read(), "Two generated PDFs should differ"
|
||||||
|
finally:
|
||||||
|
os.unlink(path1)
|
||||||
|
os.unlink(path2)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Tests for app/tasks/imap_tasks.py module."""
|
"""Tests for app/tasks/imap_tasks.py module."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import pytest
|
import pytest
|
||||||
@@ -19,6 +20,8 @@ from app.tasks.imap_tasks import (
|
|||||||
get_capabilities,
|
get_capabilities,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_TEST_CREDENTIAL = "pass" # noqa: S105
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
class TestCleanupOldEntries:
|
class TestCleanupOldEntries:
|
||||||
@@ -84,7 +87,7 @@ class TestCheckAndPullMailbox:
|
|||||||
host=None,
|
host=None,
|
||||||
port=993,
|
port=993,
|
||||||
username="user",
|
username="user",
|
||||||
password="pass",
|
password=_TEST_CREDENTIAL,
|
||||||
use_ssl=True,
|
use_ssl=True,
|
||||||
delete_after_process=False,
|
delete_after_process=False,
|
||||||
)
|
)
|
||||||
@@ -112,7 +115,7 @@ class TestCheckAndPullMailbox:
|
|||||||
host="imap.example.com",
|
host="imap.example.com",
|
||||||
port=993,
|
port=993,
|
||||||
username="user",
|
username="user",
|
||||||
password="pass",
|
password=_TEST_CREDENTIAL,
|
||||||
use_ssl=True,
|
use_ssl=True,
|
||||||
delete_after_process=False,
|
delete_after_process=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ Tests notification utilities and URL masking.
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import Mock, patch, MagicMock
|
from unittest.mock import Mock, patch, MagicMock
|
||||||
|
|
||||||
|
_TEST_CREDENTIAL_URL = "https://user:password@example.com/notify" # noqa: S105
|
||||||
|
_TEST_QUERY_URL = "https://example.com/api?key=secret1&password=secret2&public=visible" # noqa: S105
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
class TestNotificationUrlMasking:
|
class TestNotificationUrlMasking:
|
||||||
@@ -16,7 +19,7 @@ class TestNotificationUrlMasking:
|
|||||||
"""Test masking of basic auth URLs"""
|
"""Test masking of basic auth URLs"""
|
||||||
from app.utils.notification import _mask_sensitive_url
|
from app.utils.notification import _mask_sensitive_url
|
||||||
|
|
||||||
url = "https://user:password@example.com/notify"
|
url = _TEST_CREDENTIAL_URL
|
||||||
masked = _mask_sensitive_url(url)
|
masked = _mask_sensitive_url(url)
|
||||||
|
|
||||||
# Password should be masked
|
# Password should be masked
|
||||||
@@ -75,7 +78,7 @@ class TestNotificationUrlMasking:
|
|||||||
"""Test masking with multiple sensitive parameters"""
|
"""Test masking with multiple sensitive parameters"""
|
||||||
from app.utils.notification import _mask_sensitive_url
|
from app.utils.notification import _mask_sensitive_url
|
||||||
|
|
||||||
url = "https://example.com/api?key=secret1&password=secret2&public=visible"
|
url = _TEST_QUERY_URL
|
||||||
masked = _mask_sensitive_url(url)
|
masked = _mask_sensitive_url(url)
|
||||||
|
|
||||||
# Sensitive params should be masked
|
# Sensitive params should be masked
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class TestFilenameSanitization:
|
|||||||
|
|
||||||
result = sanitize_filename("/etc/passwd")
|
result = sanitize_filename("/etc/passwd")
|
||||||
assert "/" not in result
|
assert "/" not in result
|
||||||
assert result == "_etc_passwd"
|
assert result == "etc_passwd"
|
||||||
|
|
||||||
def test_sanitize_removes_windows_path_separators(self):
|
def test_sanitize_removes_windows_path_separators(self):
|
||||||
"""Test that Windows path separators are removed."""
|
"""Test that Windows path separators are removed."""
|
||||||
@@ -83,9 +83,9 @@ class TestFilenameSanitization:
|
|||||||
from app.utils.filename_utils import sanitize_filename
|
from app.utils.filename_utils import sanitize_filename
|
||||||
|
|
||||||
# Unicode fullwidth solidus (looks like /)
|
# Unicode fullwidth solidus (looks like /)
|
||||||
result = sanitize_filename("folder\uFF0Ffile.pdf")
|
result = sanitize_filename("folder\uff0ffile.pdf")
|
||||||
# Should be replaced with underscore
|
# Should be replaced with underscore
|
||||||
assert "\uFF0F" not in result
|
assert "\uff0f" not in result
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.security
|
@pytest.mark.security
|
||||||
@@ -187,12 +187,8 @@ class TestEmbedMetadataPathTraversal:
|
|||||||
processed_dir = tmp_path / "processed"
|
processed_dir = tmp_path / "processed"
|
||||||
processed_dir.mkdir()
|
processed_dir.mkdir()
|
||||||
|
|
||||||
# Execute task
|
# Execute task (called directly, Celery injects 'self' automatically)
|
||||||
task_mock = MagicMock()
|
|
||||||
task_mock.request.id = "test-task-id"
|
|
||||||
|
|
||||||
result = embed_metadata_into_pdf(
|
result = embed_metadata_into_pdf(
|
||||||
task_mock,
|
|
||||||
str(test_pdf),
|
str(test_pdf),
|
||||||
"test text",
|
"test text",
|
||||||
malicious_metadata,
|
malicious_metadata,
|
||||||
@@ -222,7 +218,7 @@ class TestExtractMetadataFilenameValidation:
|
|||||||
|
|
||||||
# Valid pattern from extract_metadata_with_gpt.py
|
# Valid pattern from extract_metadata_with_gpt.py
|
||||||
# TODO: Consider extracting this to a shared constant to avoid duplication
|
# TODO: Consider extracting this to a shared constant to avoid duplication
|
||||||
valid_pattern = r'^[\w\-\. ]+$'
|
valid_pattern = r"^[\w\-\. ]+$"
|
||||||
|
|
||||||
# Test valid filenames
|
# Test valid filenames
|
||||||
valid_filenames = [
|
valid_filenames = [
|
||||||
@@ -365,10 +361,11 @@ class TestFileUploadSecurity:
|
|||||||
"""Test that ui_upload extracts basename to prevent path traversal."""
|
"""Test that ui_upload extracts basename to prevent path traversal."""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from app.utils.filename_utils import sanitize_filename
|
||||||
|
|
||||||
# Simulate malicious filenames
|
# Simulate malicious filenames
|
||||||
malicious_filenames = [
|
malicious_filenames = [
|
||||||
"../../../etc/passwd",
|
"../../../etc/passwd",
|
||||||
"..\\..\\windows\\system32",
|
|
||||||
"/etc/shadow",
|
"/etc/shadow",
|
||||||
"folder/../file.pdf",
|
"folder/../file.pdf",
|
||||||
]
|
]
|
||||||
@@ -380,7 +377,17 @@ class TestFileUploadSecurity:
|
|||||||
# Verify no path traversal remains in basename
|
# Verify no path traversal remains in basename
|
||||||
assert ".." not in basename, f"Path traversal not removed: {malicious} -> {basename}"
|
assert ".." not in basename, f"Path traversal not removed: {malicious} -> {basename}"
|
||||||
assert "/" not in basename, f"Path separator not removed: {malicious} -> {basename}"
|
assert "/" not in basename, f"Path separator not removed: {malicious} -> {basename}"
|
||||||
assert "\\" not in basename, f"Path separator not removed: {malicious} -> {basename}"
|
|
||||||
|
# Windows-style backslash paths: os.path.basename on Linux does NOT
|
||||||
|
# split on backslash, so the application also uses sanitize_filename
|
||||||
|
# to handle these. Verify the combined approach is safe.
|
||||||
|
windows_paths = [
|
||||||
|
"..\\..\\windows\\system32",
|
||||||
|
]
|
||||||
|
for malicious in windows_paths:
|
||||||
|
sanitized = sanitize_filename(os.path.basename(malicious))
|
||||||
|
assert ".." not in sanitized, f"Path traversal not removed after sanitize: {malicious} -> {sanitized}"
|
||||||
|
assert "\\" not in sanitized, f"Backslash not removed after sanitize: {malicious} -> {sanitized}"
|
||||||
|
|
||||||
def test_sanitize_after_basename(self):
|
def test_sanitize_after_basename(self):
|
||||||
"""Test that sanitization happens after basename extraction."""
|
"""Test that sanitization happens after basename extraction."""
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ def test_rate_limit_configuration():
|
|||||||
assert hasattr(settings, "rate_limiting_enabled")
|
assert hasattr(settings, "rate_limiting_enabled")
|
||||||
assert hasattr(settings, "rate_limit_default")
|
assert hasattr(settings, "rate_limit_default")
|
||||||
assert hasattr(settings, "rate_limit_upload")
|
assert hasattr(settings, "rate_limit_upload")
|
||||||
assert hasattr(settings, "rate_limit_process")
|
|
||||||
assert hasattr(settings, "rate_limit_auth")
|
assert hasattr(settings, "rate_limit_auth")
|
||||||
|
|
||||||
# Verify that settings are strings in correct format
|
# Verify that settings are strings in correct format
|
||||||
@@ -40,8 +39,6 @@ def test_rate_limit_configuration():
|
|||||||
assert "/" in settings.rate_limit_default # Should be like "100/minute"
|
assert "/" in settings.rate_limit_default # Should be like "100/minute"
|
||||||
assert isinstance(settings.rate_limit_upload, str)
|
assert isinstance(settings.rate_limit_upload, str)
|
||||||
assert "/" in settings.rate_limit_upload
|
assert "/" in settings.rate_limit_upload
|
||||||
assert isinstance(settings.rate_limit_process, str)
|
|
||||||
assert "/" in settings.rate_limit_process
|
|
||||||
assert isinstance(settings.rate_limit_auth, str)
|
assert isinstance(settings.rate_limit_auth, str)
|
||||||
assert "/" in settings.rate_limit_auth
|
assert "/" in settings.rate_limit_auth
|
||||||
|
|
||||||
@@ -224,7 +221,6 @@ def test_rate_limit_format_validation():
|
|||||||
|
|
||||||
assert validate_rate_limit(settings.rate_limit_default)
|
assert validate_rate_limit(settings.rate_limit_default)
|
||||||
assert validate_rate_limit(settings.rate_limit_upload)
|
assert validate_rate_limit(settings.rate_limit_upload)
|
||||||
assert validate_rate_limit(settings.rate_limit_process)
|
|
||||||
assert validate_rate_limit(settings.rate_limit_auth)
|
assert validate_rate_limit(settings.rate_limit_auth)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+94
-67
@@ -13,13 +13,16 @@ from app.tasks.upload_to_webdav import upload_to_webdav
|
|||||||
from app.tasks.upload_to_google_drive import upload_to_google_drive
|
from app.tasks.upload_to_google_drive import upload_to_google_drive
|
||||||
from app.tasks.upload_to_email import upload_to_email
|
from app.tasks.upload_to_email import upload_to_email
|
||||||
|
|
||||||
|
_TEST_CREDENTIAL = "test_pass" # noqa: S105
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_settings():
|
def mock_settings():
|
||||||
"""Mock settings for upload tests."""
|
"""Mock settings for upload tests."""
|
||||||
with patch("app.tasks.upload_to_onedrive.settings") as onedrive_settings, patch(
|
with (
|
||||||
"app.tasks.upload_to_s3.settings"
|
patch("app.tasks.upload_to_onedrive.settings") as onedrive_settings,
|
||||||
) as s3_settings:
|
patch("app.tasks.upload_to_s3.settings") as s3_settings,
|
||||||
|
):
|
||||||
# OneDrive settings
|
# OneDrive settings
|
||||||
onedrive_settings.onedrive_client_id = "test_client_id"
|
onedrive_settings.onedrive_client_id = "test_client_id"
|
||||||
onedrive_settings.onedrive_client_secret = "test_secret"
|
onedrive_settings.onedrive_client_secret = "test_secret"
|
||||||
@@ -42,10 +45,11 @@ def mock_settings():
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_onedrive_accepts_file_id(sample_text_file, mock_settings):
|
def test_upload_to_onedrive_accepts_file_id(sample_text_file, mock_settings):
|
||||||
"""Test that upload_to_onedrive accepts file_id parameter."""
|
"""Test that upload_to_onedrive accepts file_id parameter."""
|
||||||
with patch("app.tasks.upload_to_onedrive.get_onedrive_token") as mock_token, patch(
|
with (
|
||||||
"app.tasks.upload_to_onedrive.create_upload_session"
|
patch("app.tasks.upload_to_onedrive.get_onedrive_token") as mock_token,
|
||||||
) as mock_session, patch("app.tasks.upload_to_onedrive.upload_large_file") as mock_upload, patch(
|
patch("app.tasks.upload_to_onedrive.create_upload_session") as mock_session,
|
||||||
"app.tasks.upload_to_onedrive.log_task_progress"
|
patch("app.tasks.upload_to_onedrive.upload_large_file") as mock_upload,
|
||||||
|
patch("app.tasks.upload_to_onedrive.log_task_progress"),
|
||||||
):
|
):
|
||||||
|
|
||||||
# Setup mocks
|
# Setup mocks
|
||||||
@@ -65,10 +69,11 @@ def test_upload_to_onedrive_accepts_file_id(sample_text_file, mock_settings):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_onedrive_without_file_id(sample_text_file, mock_settings):
|
def test_upload_to_onedrive_without_file_id(sample_text_file, mock_settings):
|
||||||
"""Test that upload_to_onedrive works without file_id parameter."""
|
"""Test that upload_to_onedrive works without file_id parameter."""
|
||||||
with patch("app.tasks.upload_to_onedrive.get_onedrive_token") as mock_token, patch(
|
with (
|
||||||
"app.tasks.upload_to_onedrive.create_upload_session"
|
patch("app.tasks.upload_to_onedrive.get_onedrive_token") as mock_token,
|
||||||
) as mock_session, patch("app.tasks.upload_to_onedrive.upload_large_file") as mock_upload, patch(
|
patch("app.tasks.upload_to_onedrive.create_upload_session") as mock_session,
|
||||||
"app.tasks.upload_to_onedrive.log_task_progress"
|
patch("app.tasks.upload_to_onedrive.upload_large_file") as mock_upload,
|
||||||
|
patch("app.tasks.upload_to_onedrive.log_task_progress"),
|
||||||
):
|
):
|
||||||
|
|
||||||
# Setup mocks
|
# Setup mocks
|
||||||
@@ -86,8 +91,9 @@ def test_upload_to_onedrive_without_file_id(sample_text_file, mock_settings):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_s3_accepts_file_id(sample_text_file, mock_settings):
|
def test_upload_to_s3_accepts_file_id(sample_text_file, mock_settings):
|
||||||
"""Test that upload_to_s3 accepts file_id parameter."""
|
"""Test that upload_to_s3 accepts file_id parameter."""
|
||||||
with patch("app.tasks.upload_to_s3.boto3.client") as mock_boto_client, patch(
|
with (
|
||||||
"app.tasks.upload_to_s3.log_task_progress"
|
patch("app.tasks.upload_to_s3.boto3.client") as mock_boto_client,
|
||||||
|
patch("app.tasks.upload_to_s3.log_task_progress"),
|
||||||
):
|
):
|
||||||
|
|
||||||
# Setup mock S3 client
|
# Setup mock S3 client
|
||||||
@@ -107,8 +113,9 @@ def test_upload_to_s3_accepts_file_id(sample_text_file, mock_settings):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_s3_without_file_id(sample_text_file, mock_settings):
|
def test_upload_to_s3_without_file_id(sample_text_file, mock_settings):
|
||||||
"""Test that upload_to_s3 works without file_id parameter."""
|
"""Test that upload_to_s3 works without file_id parameter."""
|
||||||
with patch("app.tasks.upload_to_s3.boto3.client") as mock_boto_client, patch(
|
with (
|
||||||
"app.tasks.upload_to_s3.log_task_progress"
|
patch("app.tasks.upload_to_s3.boto3.client") as mock_boto_client,
|
||||||
|
patch("app.tasks.upload_to_s3.log_task_progress"),
|
||||||
):
|
):
|
||||||
|
|
||||||
# Setup mock S3 client
|
# Setup mock S3 client
|
||||||
@@ -142,11 +149,12 @@ def test_upload_to_s3_file_not_found(mock_settings):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_onedrive_logs_with_file_id(sample_text_file, mock_settings):
|
def test_upload_to_onedrive_logs_with_file_id(sample_text_file, mock_settings):
|
||||||
"""Test that upload_to_onedrive properly logs with file_id."""
|
"""Test that upload_to_onedrive properly logs with file_id."""
|
||||||
with patch("app.tasks.upload_to_onedrive.get_onedrive_token") as mock_token, patch(
|
with (
|
||||||
"app.tasks.upload_to_onedrive.create_upload_session"
|
patch("app.tasks.upload_to_onedrive.get_onedrive_token") as mock_token,
|
||||||
) as mock_session, patch("app.tasks.upload_to_onedrive.upload_large_file") as mock_upload, patch(
|
patch("app.tasks.upload_to_onedrive.create_upload_session") as mock_session,
|
||||||
"app.tasks.upload_to_onedrive.log_task_progress"
|
patch("app.tasks.upload_to_onedrive.upload_large_file") as mock_upload,
|
||||||
) as mock_log:
|
patch("app.tasks.upload_to_onedrive.log_task_progress") as mock_log,
|
||||||
|
):
|
||||||
|
|
||||||
# Setup mocks
|
# Setup mocks
|
||||||
mock_token.return_value = "test_access_token"
|
mock_token.return_value = "test_access_token"
|
||||||
@@ -168,9 +176,10 @@ def test_upload_to_onedrive_logs_with_file_id(sample_text_file, mock_settings):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_s3_logs_with_file_id(sample_text_file, mock_settings):
|
def test_upload_to_s3_logs_with_file_id(sample_text_file, mock_settings):
|
||||||
"""Test that upload_to_s3 properly logs with file_id."""
|
"""Test that upload_to_s3 properly logs with file_id."""
|
||||||
with patch("app.tasks.upload_to_s3.boto3.client") as mock_boto_client, patch(
|
with (
|
||||||
"app.tasks.upload_to_s3.log_task_progress"
|
patch("app.tasks.upload_to_s3.boto3.client") as mock_boto_client,
|
||||||
) as mock_log:
|
patch("app.tasks.upload_to_s3.log_task_progress") as mock_log,
|
||||||
|
):
|
||||||
|
|
||||||
# Setup mock S3 client
|
# Setup mock S3 client
|
||||||
mock_s3 = Mock()
|
mock_s3 = Mock()
|
||||||
@@ -191,18 +200,21 @@ def test_upload_to_s3_logs_with_file_id(sample_text_file, mock_settings):
|
|||||||
|
|
||||||
# Tests for newly standardized upload tasks
|
# Tests for newly standardized upload tasks
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_ftp_accepts_file_id(sample_text_file):
|
def test_upload_to_ftp_accepts_file_id(sample_text_file):
|
||||||
"""Test that upload_to_ftp accepts file_id parameter."""
|
"""Test that upload_to_ftp accepts file_id parameter."""
|
||||||
with patch("app.tasks.upload_to_ftp.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_ftp.ftplib.FTP") as mock_ftp, \
|
patch("app.tasks.upload_to_ftp.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_ftp.log_task_progress"):
|
patch("app.tasks.upload_to_ftp.ftplib.FTP") as mock_ftp,
|
||||||
|
patch("app.tasks.upload_to_ftp.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
# Setup settings
|
# Setup settings
|
||||||
mock_settings.ftp_host = "ftp.example.com"
|
mock_settings.ftp_host = "ftp.example.com"
|
||||||
mock_settings.ftp_port = 21
|
mock_settings.ftp_port = 21
|
||||||
mock_settings.ftp_username = "test_user"
|
mock_settings.ftp_username = "test_user"
|
||||||
mock_settings.ftp_password = "test_pass"
|
mock_settings.ftp_password = _TEST_CREDENTIAL
|
||||||
mock_settings.ftp_folder = "uploads"
|
mock_settings.ftp_folder = "uploads"
|
||||||
mock_settings.ftp_use_tls = False
|
mock_settings.ftp_use_tls = False
|
||||||
mock_settings.ftp_allow_plaintext = True
|
mock_settings.ftp_allow_plaintext = True
|
||||||
@@ -222,14 +234,16 @@ def test_upload_to_ftp_accepts_file_id(sample_text_file):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_ftp_without_file_id(sample_text_file):
|
def test_upload_to_ftp_without_file_id(sample_text_file):
|
||||||
"""Test that upload_to_ftp works without file_id parameter."""
|
"""Test that upload_to_ftp works without file_id parameter."""
|
||||||
with patch("app.tasks.upload_to_ftp.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_ftp.ftplib.FTP") as mock_ftp, \
|
patch("app.tasks.upload_to_ftp.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_ftp.log_task_progress"):
|
patch("app.tasks.upload_to_ftp.ftplib.FTP") as mock_ftp,
|
||||||
|
patch("app.tasks.upload_to_ftp.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
# Setup settings
|
# Setup settings
|
||||||
mock_settings.ftp_host = "ftp.example.com"
|
mock_settings.ftp_host = "ftp.example.com"
|
||||||
mock_settings.ftp_username = "test_user"
|
mock_settings.ftp_username = "test_user"
|
||||||
mock_settings.ftp_password = "test_pass"
|
mock_settings.ftp_password = _TEST_CREDENTIAL
|
||||||
mock_settings.ftp_folder = None
|
mock_settings.ftp_folder = None
|
||||||
mock_settings.ftp_use_tls = False
|
mock_settings.ftp_use_tls = False
|
||||||
mock_settings.ftp_allow_plaintext = True
|
mock_settings.ftp_allow_plaintext = True
|
||||||
@@ -247,17 +261,19 @@ def test_upload_to_ftp_without_file_id(sample_text_file):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_sftp_accepts_file_id(sample_text_file):
|
def test_upload_to_sftp_accepts_file_id(sample_text_file):
|
||||||
"""Test that upload_to_sftp accepts file_id parameter."""
|
"""Test that upload_to_sftp accepts file_id parameter."""
|
||||||
with patch("app.tasks.upload_to_sftp.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_sftp.paramiko.SSHClient") as mock_ssh, \
|
patch("app.tasks.upload_to_sftp.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_sftp.log_task_progress"), \
|
patch("app.tasks.upload_to_sftp.paramiko.SSHClient") as mock_ssh,
|
||||||
patch("app.tasks.upload_to_sftp.extract_remote_path") as mock_extract, \
|
patch("app.tasks.upload_to_sftp.log_task_progress"),
|
||||||
patch("app.tasks.upload_to_sftp.get_unique_filename") as mock_unique:
|
patch("app.tasks.upload_to_sftp.extract_remote_path") as mock_extract,
|
||||||
|
patch("app.tasks.upload_to_sftp.get_unique_filename") as mock_unique,
|
||||||
|
):
|
||||||
|
|
||||||
# Setup settings
|
# Setup settings
|
||||||
mock_settings.sftp_host = "sftp.example.com"
|
mock_settings.sftp_host = "sftp.example.com"
|
||||||
mock_settings.sftp_port = 22
|
mock_settings.sftp_port = 22
|
||||||
mock_settings.sftp_username = "test_user"
|
mock_settings.sftp_username = "test_user"
|
||||||
mock_settings.sftp_password = "test_pass"
|
mock_settings.sftp_password = _TEST_CREDENTIAL
|
||||||
mock_settings.sftp_folder = "/uploads"
|
mock_settings.sftp_folder = "/uploads"
|
||||||
mock_settings.workdir = "/tmp"
|
mock_settings.workdir = "/tmp"
|
||||||
|
|
||||||
@@ -280,14 +296,16 @@ def test_upload_to_sftp_accepts_file_id(sample_text_file):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_webdav_accepts_file_id(sample_text_file):
|
def test_upload_to_webdav_accepts_file_id(sample_text_file):
|
||||||
"""Test that upload_to_webdav accepts file_id parameter."""
|
"""Test that upload_to_webdav accepts file_id parameter."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
# Setup settings
|
# Setup settings
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
|
|
||||||
@@ -307,11 +325,13 @@ def test_upload_to_webdav_accepts_file_id(sample_text_file):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_google_drive_accepts_file_id(sample_text_file):
|
def test_upload_to_google_drive_accepts_file_id(sample_text_file):
|
||||||
"""Test that upload_to_google_drive accepts file_id parameter."""
|
"""Test that upload_to_google_drive accepts file_id parameter."""
|
||||||
with patch("app.tasks.upload_to_google_drive.get_google_drive_service") as mock_service, \
|
with (
|
||||||
patch("app.tasks.upload_to_google_drive.MediaFileUpload") as mock_media, \
|
patch("app.tasks.upload_to_google_drive.get_google_drive_service") as mock_service,
|
||||||
patch("app.tasks.upload_to_google_drive.extract_metadata_from_file") as mock_metadata, \
|
patch("app.tasks.upload_to_google_drive.MediaFileUpload"),
|
||||||
patch("app.tasks.upload_to_google_drive.settings") as mock_settings, \
|
patch("app.tasks.upload_to_google_drive.extract_metadata_from_file") as mock_metadata,
|
||||||
patch("app.tasks.upload_to_google_drive.log_task_progress"):
|
patch("app.tasks.upload_to_google_drive.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_google_drive.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
# Setup settings
|
# Setup settings
|
||||||
mock_settings.google_drive_folder_id = "test_folder_id"
|
mock_settings.google_drive_folder_id = "test_folder_id"
|
||||||
@@ -328,7 +348,7 @@ def test_upload_to_google_drive_accepts_file_id(sample_text_file):
|
|||||||
mock_create.execute.return_value = {
|
mock_create.execute.return_value = {
|
||||||
"id": "file123",
|
"id": "file123",
|
||||||
"name": "test.txt",
|
"name": "test.txt",
|
||||||
"webViewLink": "https://drive.google.com/file/d/file123"
|
"webViewLink": "https://drive.google.com/file/d/file123",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Call with file_id parameter
|
# Call with file_id parameter
|
||||||
@@ -342,20 +362,22 @@ def test_upload_to_google_drive_accepts_file_id(sample_text_file):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_email_accepts_file_id(sample_text_file):
|
def test_upload_to_email_accepts_file_id(sample_text_file):
|
||||||
"""Test that upload_to_email accepts file_id parameter."""
|
"""Test that upload_to_email accepts file_id parameter."""
|
||||||
with patch("app.tasks.upload_to_email.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_email.smtplib.SMTP") as mock_smtp, \
|
patch("app.tasks.upload_to_email.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_email.get_email_template") as mock_template, \
|
patch("app.tasks.upload_to_email.smtplib.SMTP"),
|
||||||
patch("app.tasks.upload_to_email.extract_metadata_from_file") as mock_metadata, \
|
patch("app.tasks.upload_to_email.get_email_template") as mock_template,
|
||||||
patch("app.tasks.upload_to_email.log_task_progress"), \
|
patch("app.tasks.upload_to_email.extract_metadata_from_file") as mock_metadata,
|
||||||
patch("app.tasks.upload_to_email._prepare_recipients") as mock_recipients, \
|
patch("app.tasks.upload_to_email.log_task_progress"),
|
||||||
patch("app.tasks.upload_to_email._send_email_with_smtp") as mock_send, \
|
patch("app.tasks.upload_to_email._prepare_recipients") as mock_recipients,
|
||||||
patch("app.tasks.upload_to_email.attach_logo") as mock_logo:
|
patch("app.tasks.upload_to_email._send_email_with_smtp") as mock_send,
|
||||||
|
patch("app.tasks.upload_to_email.attach_logo") as mock_logo,
|
||||||
|
):
|
||||||
|
|
||||||
# Setup settings
|
# Setup settings
|
||||||
mock_settings.email_host = "smtp.example.com"
|
mock_settings.email_host = "smtp.example.com"
|
||||||
mock_settings.email_port = 587
|
mock_settings.email_port = 587
|
||||||
mock_settings.email_username = "test@example.com"
|
mock_settings.email_username = "test@example.com"
|
||||||
mock_settings.email_password = "test_pass"
|
mock_settings.email_password = _TEST_CREDENTIAL
|
||||||
mock_settings.email_use_tls = True
|
mock_settings.email_use_tls = True
|
||||||
mock_settings.email_sender = "sender@example.com"
|
mock_settings.email_sender = "sender@example.com"
|
||||||
mock_settings.external_hostname = "docuelevate.example.com"
|
mock_settings.external_hostname = "docuelevate.example.com"
|
||||||
@@ -381,8 +403,7 @@ def test_upload_to_email_accepts_file_id(sample_text_file):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_ftp_file_not_found():
|
def test_upload_to_ftp_file_not_found():
|
||||||
"""Test that upload_to_ftp raises error for missing file."""
|
"""Test that upload_to_ftp raises error for missing file."""
|
||||||
with patch("app.tasks.upload_to_ftp.settings") as mock_settings, \
|
with patch("app.tasks.upload_to_ftp.settings") as mock_settings, patch("app.tasks.upload_to_ftp.log_task_progress"):
|
||||||
patch("app.tasks.upload_to_ftp.log_task_progress"):
|
|
||||||
|
|
||||||
mock_settings.ftp_host = "ftp.example.com"
|
mock_settings.ftp_host = "ftp.example.com"
|
||||||
|
|
||||||
@@ -393,8 +414,10 @@ def test_upload_to_ftp_file_not_found():
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_sftp_file_not_found():
|
def test_upload_to_sftp_file_not_found():
|
||||||
"""Test that upload_to_sftp raises error for missing file."""
|
"""Test that upload_to_sftp raises error for missing file."""
|
||||||
with patch("app.tasks.upload_to_sftp.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_sftp.log_task_progress"):
|
patch("app.tasks.upload_to_sftp.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_sftp.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.sftp_host = "sftp.example.com"
|
mock_settings.sftp_host = "sftp.example.com"
|
||||||
mock_settings.sftp_port = 22
|
mock_settings.sftp_port = 22
|
||||||
@@ -407,8 +430,10 @@ def test_upload_to_sftp_file_not_found():
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_upload_to_webdav_file_not_found():
|
def test_upload_to_webdav_file_not_found():
|
||||||
"""Test that upload_to_webdav raises error for missing file."""
|
"""Test that upload_to_webdav raises error for missing file."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
|
|
||||||
@@ -470,11 +495,13 @@ def test_send_to_all_calls_upload_tasks_with_keyword_argument():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Mock all the upload functions and settings
|
# Mock all the upload functions and settings
|
||||||
with patch("app.tasks.send_to_all.upload_to_s3") as mock_s3, \
|
with (
|
||||||
patch("app.tasks.send_to_all.settings") as mock_settings, \
|
patch("app.tasks.send_to_all.upload_to_s3") as mock_s3,
|
||||||
patch("app.tasks.send_to_all.log_task_progress"), \
|
patch("app.tasks.send_to_all.settings") as mock_settings,
|
||||||
patch("app.tasks.send_to_all.SessionLocal"), \
|
patch("app.tasks.send_to_all.log_task_progress"),
|
||||||
patch("app.tasks.send_to_all.get_configured_services_from_validator") as mock_validator:
|
patch("app.tasks.send_to_all.SessionLocal"),
|
||||||
|
patch("app.tasks.send_to_all.get_configured_services_from_validator") as mock_validator,
|
||||||
|
):
|
||||||
|
|
||||||
# Configure validator to return S3 as configured
|
# Configure validator to return S3 as configured
|
||||||
mock_validator.return_value = {"s3": True}
|
mock_validator.return_value = {"s3": True}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Comprehensive tests for upload_to_webdav task."""
|
"""Comprehensive tests for upload_to_webdav task."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch, Mock, MagicMock
|
from unittest.mock import patch, Mock, MagicMock
|
||||||
@@ -6,6 +7,9 @@ from requests.exceptions import ConnectionError, Timeout, RequestException
|
|||||||
|
|
||||||
from app.tasks.upload_to_webdav import upload_to_webdav
|
from app.tasks.upload_to_webdav import upload_to_webdav
|
||||||
|
|
||||||
|
_TEST_CREDENTIAL = "test_pass" # noqa: S105
|
||||||
|
_TEST_CUSTOM_CREDENTIAL = "custom_password123" # noqa: S105
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
class TestUploadToWebDAV:
|
class TestUploadToWebDAV:
|
||||||
@@ -13,14 +17,16 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_upload_success_with_file_id(self, sample_text_file):
|
def test_upload_success_with_file_id(self, sample_text_file):
|
||||||
"""Test successful upload with file_id parameter."""
|
"""Test successful upload with file_id parameter."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress") as mock_log:
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress") as mock_log,
|
||||||
|
):
|
||||||
|
|
||||||
# Setup settings
|
# Setup settings
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -42,27 +48,26 @@ class TestUploadToWebDAV:
|
|||||||
# Verify requests.put was called correctly
|
# Verify requests.put was called correctly
|
||||||
assert mock_put.called
|
assert mock_put.called
|
||||||
call_args = mock_put.call_args
|
call_args = mock_put.call_args
|
||||||
assert call_args[1]["auth"] == ("test_user", "test_pass")
|
assert call_args[1]["auth"] == ("test_user", _TEST_CREDENTIAL)
|
||||||
assert call_args[1]["verify"] is True
|
assert call_args[1]["verify"] is True
|
||||||
assert call_args[1]["timeout"] == 30
|
assert call_args[1]["timeout"] == 30
|
||||||
|
|
||||||
# Verify logging was called with file_id
|
# Verify logging was called with file_id
|
||||||
assert mock_log.called
|
assert mock_log.called
|
||||||
log_calls_with_file_id = [
|
log_calls_with_file_id = [call for call in mock_log.call_args_list if call[1].get("file_id") == 100]
|
||||||
call for call in mock_log.call_args_list
|
|
||||||
if call[1].get("file_id") == 100
|
|
||||||
]
|
|
||||||
assert len(log_calls_with_file_id) > 0
|
assert len(log_calls_with_file_id) > 0
|
||||||
|
|
||||||
def test_upload_success_without_file_id(self, sample_text_file):
|
def test_upload_success_without_file_id(self, sample_text_file):
|
||||||
"""Test successful upload without file_id parameter."""
|
"""Test successful upload without file_id parameter."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = ""
|
mock_settings.webdav_folder = ""
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -78,13 +83,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_upload_success_status_204(self, sample_text_file):
|
def test_upload_success_status_204(self, sample_text_file):
|
||||||
"""Test successful upload with 204 No Content status."""
|
"""Test successful upload with 204 No Content status."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = None
|
mock_settings.webdav_folder = None
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -99,20 +106,24 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_missing_webdav_url(self, sample_text_file):
|
def test_missing_webdav_url(self, sample_text_file):
|
||||||
"""Test that missing WebDAV URL raises ValueError."""
|
"""Test that missing WebDAV URL raises ValueError."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = None
|
mock_settings.webdav_url = None
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="WebDAV URL is not configured"):
|
with pytest.raises(ValueError, match="WebDAV URL is not configured"):
|
||||||
upload_to_webdav.apply(args=[sample_text_file]).get()
|
upload_to_webdav.apply(args=[sample_text_file]).get()
|
||||||
|
|
||||||
def test_file_not_found(self):
|
def test_file_not_found(self):
|
||||||
"""Test that missing file raises FileNotFoundError."""
|
"""Test that missing file raises FileNotFoundError."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
|
|
||||||
@@ -121,13 +132,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_http_error_response(self, sample_text_file):
|
def test_http_error_response(self, sample_text_file):
|
||||||
"""Test handling of HTTP error responses (4xx, 5xx)."""
|
"""Test handling of HTTP error responses (4xx, 5xx)."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -143,13 +156,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_http_404_not_found(self, sample_text_file):
|
def test_http_404_not_found(self, sample_text_file):
|
||||||
"""Test handling of 404 Not Found response."""
|
"""Test handling of 404 Not Found response."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "nonexistent"
|
mock_settings.webdav_folder = "nonexistent"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -164,13 +179,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_http_500_server_error(self, sample_text_file):
|
def test_http_500_server_error(self, sample_text_file):
|
||||||
"""Test handling of 500 Internal Server Error."""
|
"""Test handling of 500 Internal Server Error."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -185,13 +202,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_connection_error(self, sample_text_file):
|
def test_connection_error(self, sample_text_file):
|
||||||
"""Test handling of connection errors."""
|
"""Test handling of connection errors."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -204,13 +223,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_timeout_error(self, sample_text_file):
|
def test_timeout_error(self, sample_text_file):
|
||||||
"""Test handling of timeout errors."""
|
"""Test handling of timeout errors."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -223,13 +244,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_url_construction_with_trailing_slash(self, sample_text_file):
|
def test_url_construction_with_trailing_slash(self, sample_text_file):
|
||||||
"""Test URL construction when base URL has trailing slash."""
|
"""Test URL construction when base URL has trailing slash."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -248,13 +271,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_url_construction_without_trailing_slash(self, sample_text_file):
|
def test_url_construction_without_trailing_slash(self, sample_text_file):
|
||||||
"""Test URL construction when base URL has no trailing slash."""
|
"""Test URL construction when base URL has no trailing slash."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com"
|
mock_settings.webdav_url = "https://webdav.example.com"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "documents"
|
mock_settings.webdav_folder = "documents"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -272,13 +297,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_folder_path_with_leading_slash(self, sample_text_file):
|
def test_folder_path_with_leading_slash(self, sample_text_file):
|
||||||
"""Test folder path normalization when it starts with /."""
|
"""Test folder path normalization when it starts with /."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "/uploads/documents" # Leading slash
|
mock_settings.webdav_folder = "/uploads/documents" # Leading slash
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -296,13 +323,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_empty_folder_path(self, sample_text_file):
|
def test_empty_folder_path(self, sample_text_file):
|
||||||
"""Test upload with empty folder path (root directory)."""
|
"""Test upload with empty folder path (root directory)."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "" # Empty folder
|
mock_settings.webdav_folder = "" # Empty folder
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -317,13 +346,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_ssl_verification_enabled(self, sample_text_file):
|
def test_ssl_verification_enabled(self, sample_text_file):
|
||||||
"""Test that SSL verification is enabled when configured."""
|
"""Test that SSL verification is enabled when configured."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -340,13 +371,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_ssl_verification_disabled(self, sample_text_file):
|
def test_ssl_verification_disabled(self, sample_text_file):
|
||||||
"""Test that SSL verification can be disabled."""
|
"""Test that SSL verification can be disabled."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = False
|
mock_settings.webdav_verify_ssl = False
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -363,13 +396,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_authentication_credentials(self, sample_text_file):
|
def test_authentication_credentials(self, sample_text_file):
|
||||||
"""Test that authentication credentials are properly passed."""
|
"""Test that authentication credentials are properly passed."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "custom_user"
|
mock_settings.webdav_username = "custom_user"
|
||||||
mock_settings.webdav_password = "custom_password123"
|
mock_settings.webdav_password = _TEST_CUSTOM_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -382,17 +417,19 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
# Verify correct credentials were used
|
# Verify correct credentials were used
|
||||||
call_kwargs = mock_put.call_args[1]
|
call_kwargs = mock_put.call_args[1]
|
||||||
assert call_kwargs["auth"] == ("custom_user", "custom_password123")
|
assert call_kwargs["auth"] == ("custom_user", _TEST_CUSTOM_CREDENTIAL)
|
||||||
|
|
||||||
def test_logging_on_success(self, sample_text_file):
|
def test_logging_on_success(self, sample_text_file):
|
||||||
"""Test that progress is logged on successful upload."""
|
"""Test that progress is logged on successful upload."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress") as mock_log:
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress") as mock_log,
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -407,21 +444,20 @@ class TestUploadToWebDAV:
|
|||||||
assert mock_log.call_count >= 2 # At least in_progress and success
|
assert mock_log.call_count >= 2 # At least in_progress and success
|
||||||
|
|
||||||
# Check for success log
|
# Check for success log
|
||||||
success_calls = [
|
success_calls = [call for call in mock_log.call_args_list if call[0][2] == "success"]
|
||||||
call for call in mock_log.call_args_list
|
|
||||||
if call[0][2] == "success"
|
|
||||||
]
|
|
||||||
assert len(success_calls) >= 1
|
assert len(success_calls) >= 1
|
||||||
|
|
||||||
def test_logging_on_failure(self, sample_text_file):
|
def test_logging_on_failure(self, sample_text_file):
|
||||||
"""Test that progress is logged on failed upload."""
|
"""Test that progress is logged on failed upload."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress") as mock_log:
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress") as mock_log,
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -435,21 +471,20 @@ class TestUploadToWebDAV:
|
|||||||
upload_to_webdav.apply(args=[sample_text_file], kwargs={"file_id": 42}).get()
|
upload_to_webdav.apply(args=[sample_text_file], kwargs={"file_id": 42}).get()
|
||||||
|
|
||||||
# Check for failure log
|
# Check for failure log
|
||||||
failure_calls = [
|
failure_calls = [call for call in mock_log.call_args_list if call[0][2] == "failure"]
|
||||||
call for call in mock_log.call_args_list
|
|
||||||
if call[0][2] == "failure"
|
|
||||||
]
|
|
||||||
assert len(failure_calls) >= 1
|
assert len(failure_calls) >= 1
|
||||||
|
|
||||||
def test_file_content_uploaded(self, sample_text_file):
|
def test_file_content_uploaded(self, sample_text_file):
|
||||||
"""Test that file content is actually read and uploaded."""
|
"""Test that file content is actually read and uploaded."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -468,13 +503,15 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_return_value_structure(self, sample_text_file):
|
def test_return_value_structure(self, sample_text_file):
|
||||||
"""Test that return value has correct structure."""
|
"""Test that return value has correct structure."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.requests.put") as mock_put, \
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = "https://webdav.example.com/"
|
mock_settings.webdav_url = "https://webdav.example.com/"
|
||||||
mock_settings.webdav_username = "test_user"
|
mock_settings.webdav_username = "test_user"
|
||||||
mock_settings.webdav_password = "test_pass"
|
mock_settings.webdav_password = _TEST_CREDENTIAL
|
||||||
mock_settings.webdav_folder = "uploads"
|
mock_settings.webdav_folder = "uploads"
|
||||||
mock_settings.webdav_verify_ssl = True
|
mock_settings.webdav_verify_ssl = True
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
@@ -495,14 +532,11 @@ class TestUploadToWebDAV:
|
|||||||
|
|
||||||
def test_module_importable(self):
|
def test_module_importable(self):
|
||||||
"""Test that upload_to_webdav module is importable."""
|
"""Test that upload_to_webdav module is importable."""
|
||||||
from app.tasks.upload_to_webdav import upload_to_webdav
|
|
||||||
assert callable(upload_to_webdav)
|
assert callable(upload_to_webdav)
|
||||||
|
|
||||||
def test_task_has_retry_configuration(self):
|
def test_task_has_retry_configuration(self):
|
||||||
"""Test that the task has retry configuration from BaseTaskWithRetry."""
|
"""Test that the task has retry configuration from BaseTaskWithRetry."""
|
||||||
from app.tasks.upload_to_webdav import upload_to_webdav
|
|
||||||
|
|
||||||
# BaseTaskWithRetry should provide retry configuration
|
# BaseTaskWithRetry should provide retry configuration
|
||||||
assert hasattr(upload_to_webdav, 'max_retries')
|
assert hasattr(upload_to_webdav, "max_retries")
|
||||||
# BaseTaskWithRetry configures 3 retries
|
# BaseTaskWithRetry configures 3 retries
|
||||||
assert upload_to_webdav.max_retries == 3
|
assert upload_to_webdav.max_retries == 3
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Integration tests for WebDAV upload with real server.
|
|||||||
These tests spin up a real WebDAV server in a Docker container and test
|
These tests spin up a real WebDAV server in a Docker container and test
|
||||||
actual file uploads against it, then verify the files were uploaded successfully.
|
actual file uploads against it, then verify the files were uploaded successfully.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import pytest
|
import pytest
|
||||||
@@ -17,6 +18,9 @@ from app.tasks.upload_to_webdav import upload_to_webdav
|
|||||||
pytest.importorskip("testcontainers", reason="testcontainers not installed")
|
pytest.importorskip("testcontainers", reason="testcontainers not installed")
|
||||||
from testcontainers.core.container import DockerContainer
|
from testcontainers.core.container import DockerContainer
|
||||||
|
|
||||||
|
_TEST_CREDENTIAL = "testpass" # noqa: S105
|
||||||
|
_TEST_WRONG_CREDENTIAL = "wrongpass" # noqa: S105
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
@pytest.mark.requires_docker
|
@pytest.mark.requires_docker
|
||||||
@@ -35,7 +39,7 @@ class TestWebDAVIntegration:
|
|||||||
container.with_exposed_ports(80)
|
container.with_exposed_ports(80)
|
||||||
container.with_env("AUTH_TYPE", "Basic")
|
container.with_env("AUTH_TYPE", "Basic")
|
||||||
container.with_env("USERNAME", "testuser")
|
container.with_env("USERNAME", "testuser")
|
||||||
container.with_env("PASSWORD", "testpass")
|
container.with_env("PASSWORD", _TEST_CREDENTIAL)
|
||||||
|
|
||||||
# Start the container
|
# Start the container
|
||||||
container.start()
|
container.start()
|
||||||
@@ -53,18 +57,20 @@ class TestWebDAVIntegration:
|
|||||||
"port": port,
|
"port": port,
|
||||||
"url": f"http://{host}:{port}",
|
"url": f"http://{host}:{port}",
|
||||||
"username": "testuser",
|
"username": "testuser",
|
||||||
"password": "testpass"
|
"password": _TEST_CREDENTIAL,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Verify server is accessible
|
# Verify server is accessible
|
||||||
try:
|
try:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
server_info["url"],
|
server_info["url"], auth=(server_info["username"], server_info["password"]), timeout=5
|
||||||
auth=(server_info["username"], server_info["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
assert response.status_code in [200, 301, 302, 401], \
|
assert response.status_code in [
|
||||||
f"WebDAV server not ready, got status {response.status_code}"
|
200,
|
||||||
|
301,
|
||||||
|
302,
|
||||||
|
401,
|
||||||
|
], f"WebDAV server not ready, got status {response.status_code}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
container.stop()
|
container.stop()
|
||||||
pytest.fail(f"Failed to connect to WebDAV server: {e}")
|
pytest.fail(f"Failed to connect to WebDAV server: {e}")
|
||||||
@@ -76,8 +82,10 @@ class TestWebDAVIntegration:
|
|||||||
|
|
||||||
def test_upload_file_to_real_webdav_server(self, webdav_server, sample_text_file):
|
def test_upload_file_to_real_webdav_server(self, webdav_server, sample_text_file):
|
||||||
"""Test uploading a file to a real WebDAV server."""
|
"""Test uploading a file to a real WebDAV server."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
# Configure settings to point to real WebDAV server
|
# Configure settings to point to real WebDAV server
|
||||||
mock_settings.webdav_url = webdav_server["url"] + "/"
|
mock_settings.webdav_url = webdav_server["url"] + "/"
|
||||||
@@ -88,10 +96,7 @@ class TestWebDAVIntegration:
|
|||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
|
|
||||||
# Upload the file
|
# Upload the file
|
||||||
result = upload_to_webdav.apply(
|
result = upload_to_webdav.apply(args=[sample_text_file], kwargs={"file_id": 1}).get()
|
||||||
args=[sample_text_file],
|
|
||||||
kwargs={"file_id": 1}
|
|
||||||
).get()
|
|
||||||
|
|
||||||
# Verify upload succeeded
|
# Verify upload succeeded
|
||||||
assert result["status"] == "Completed"
|
assert result["status"] == "Completed"
|
||||||
@@ -102,26 +107,22 @@ class TestWebDAVIntegration:
|
|||||||
filename = os.path.basename(sample_text_file)
|
filename = os.path.basename(sample_text_file)
|
||||||
file_url = f"{webdav_server['url']}/{filename}"
|
file_url = f"{webdav_server['url']}/{filename}"
|
||||||
|
|
||||||
response = requests.get(
|
response = requests.get(file_url, auth=(webdav_server["username"], webdav_server["password"]), timeout=5)
|
||||||
file_url,
|
|
||||||
auth=(webdav_server["username"], webdav_server["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200, \
|
assert response.status_code == 200, f"File not found on server: {response.status_code}"
|
||||||
f"File not found on server: {response.status_code}"
|
|
||||||
|
|
||||||
# Verify file content matches
|
# Verify file content matches
|
||||||
with open(sample_text_file, "rb") as f:
|
with open(sample_text_file, "rb") as f:
|
||||||
expected_content = f.read()
|
expected_content = f.read()
|
||||||
|
|
||||||
assert response.content == expected_content, \
|
assert response.content == expected_content, "Uploaded file content does not match original"
|
||||||
"Uploaded file content does not match original"
|
|
||||||
|
|
||||||
def test_upload_to_subfolder(self, webdav_server, sample_text_file):
|
def test_upload_to_subfolder(self, webdav_server, sample_text_file):
|
||||||
"""Test uploading a file to a subfolder on WebDAV server."""
|
"""Test uploading a file to a subfolder on WebDAV server."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
# Create a test folder first
|
# Create a test folder first
|
||||||
folder_name = "test-uploads"
|
folder_name = "test-uploads"
|
||||||
@@ -129,10 +130,7 @@ class TestWebDAVIntegration:
|
|||||||
|
|
||||||
# Create folder using MKCOL method
|
# Create folder using MKCOL method
|
||||||
requests.request(
|
requests.request(
|
||||||
"MKCOL",
|
"MKCOL", folder_url, auth=(webdav_server["username"], webdav_server["password"]), timeout=5
|
||||||
folder_url,
|
|
||||||
auth=(webdav_server["username"], webdav_server["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configure settings
|
# Configure settings
|
||||||
@@ -144,10 +142,7 @@ class TestWebDAVIntegration:
|
|||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
|
|
||||||
# Upload the file
|
# Upload the file
|
||||||
result = upload_to_webdav.apply(
|
result = upload_to_webdav.apply(args=[sample_text_file], kwargs={"file_id": 2}).get()
|
||||||
args=[sample_text_file],
|
|
||||||
kwargs={"file_id": 2}
|
|
||||||
).get()
|
|
||||||
|
|
||||||
# Verify upload succeeded
|
# Verify upload succeeded
|
||||||
assert result["status"] == "Completed"
|
assert result["status"] == "Completed"
|
||||||
@@ -156,19 +151,16 @@ class TestWebDAVIntegration:
|
|||||||
filename = os.path.basename(sample_text_file)
|
filename = os.path.basename(sample_text_file)
|
||||||
file_url = f"{webdav_server['url']}/{folder_name}/{filename}"
|
file_url = f"{webdav_server['url']}/{folder_name}/{filename}"
|
||||||
|
|
||||||
response = requests.get(
|
response = requests.get(file_url, auth=(webdav_server["username"], webdav_server["password"]), timeout=5)
|
||||||
file_url,
|
|
||||||
auth=(webdav_server["username"], webdav_server["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200, \
|
assert response.status_code == 200, f"File not found in subfolder: {response.status_code}"
|
||||||
f"File not found in subfolder: {response.status_code}"
|
|
||||||
|
|
||||||
def test_upload_pdf_file(self, webdav_server, sample_pdf_path):
|
def test_upload_pdf_file(self, webdav_server, sample_pdf_path):
|
||||||
"""Test uploading a PDF file to WebDAV server."""
|
"""Test uploading a PDF file to WebDAV server."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = webdav_server["url"] + "/"
|
mock_settings.webdav_url = webdav_server["url"] + "/"
|
||||||
mock_settings.webdav_username = webdav_server["username"]
|
mock_settings.webdav_username = webdav_server["username"]
|
||||||
@@ -178,10 +170,7 @@ class TestWebDAVIntegration:
|
|||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
|
|
||||||
# Upload the PDF
|
# Upload the PDF
|
||||||
result = upload_to_webdav.apply(
|
result = upload_to_webdav.apply(args=[sample_pdf_path], kwargs={"file_id": 3}).get()
|
||||||
args=[sample_pdf_path],
|
|
||||||
kwargs={"file_id": 3}
|
|
||||||
).get()
|
|
||||||
|
|
||||||
# Verify upload succeeded
|
# Verify upload succeeded
|
||||||
assert result["status"] == "Completed"
|
assert result["status"] == "Completed"
|
||||||
@@ -190,41 +179,37 @@ class TestWebDAVIntegration:
|
|||||||
filename = os.path.basename(sample_pdf_path)
|
filename = os.path.basename(sample_pdf_path)
|
||||||
file_url = f"{webdav_server['url']}/{filename}"
|
file_url = f"{webdav_server['url']}/{filename}"
|
||||||
|
|
||||||
response = requests.get(
|
response = requests.get(file_url, auth=(webdav_server["username"], webdav_server["password"]), timeout=5)
|
||||||
file_url,
|
|
||||||
auth=(webdav_server["username"], webdav_server["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
# Verify it's a PDF (check magic bytes)
|
# Verify it's a PDF (check magic bytes)
|
||||||
assert response.content.startswith(b'%PDF'), \
|
assert response.content.startswith(b"%PDF"), "Uploaded file is not a valid PDF"
|
||||||
"Uploaded file is not a valid PDF"
|
|
||||||
|
|
||||||
def test_upload_with_wrong_credentials(self, webdav_server, sample_text_file):
|
def test_upload_with_wrong_credentials(self, webdav_server, sample_text_file):
|
||||||
"""Test that upload fails with wrong credentials."""
|
"""Test that upload fails with wrong credentials."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = webdav_server["url"] + "/"
|
mock_settings.webdav_url = webdav_server["url"] + "/"
|
||||||
mock_settings.webdav_username = "wronguser"
|
mock_settings.webdav_username = "wronguser"
|
||||||
mock_settings.webdav_password = "wrongpass"
|
mock_settings.webdav_password = _TEST_WRONG_CREDENTIAL
|
||||||
mock_settings.webdav_folder = ""
|
mock_settings.webdav_folder = ""
|
||||||
mock_settings.webdav_verify_ssl = False
|
mock_settings.webdav_verify_ssl = False
|
||||||
mock_settings.http_request_timeout = 30
|
mock_settings.http_request_timeout = 30
|
||||||
|
|
||||||
# Upload should fail with 401 Unauthorized
|
# Upload should fail with 401 Unauthorized
|
||||||
with pytest.raises(Exception, match="Failed to upload.*401"):
|
with pytest.raises(Exception, match="Failed to upload.*401"):
|
||||||
upload_to_webdav.apply(
|
upload_to_webdav.apply(args=[sample_text_file], kwargs={"file_id": 4}).get()
|
||||||
args=[sample_text_file],
|
|
||||||
kwargs={"file_id": 4}
|
|
||||||
).get()
|
|
||||||
|
|
||||||
def test_upload_multiple_files(self, webdav_server, sample_text_file, tmp_path):
|
def test_upload_multiple_files(self, webdav_server, sample_text_file, tmp_path):
|
||||||
"""Test uploading multiple files sequentially."""
|
"""Test uploading multiple files sequentially."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = webdav_server["url"] + "/"
|
mock_settings.webdav_url = webdav_server["url"] + "/"
|
||||||
mock_settings.webdav_username = webdav_server["username"]
|
mock_settings.webdav_username = webdav_server["username"]
|
||||||
@@ -248,10 +233,7 @@ class TestWebDAVIntegration:
|
|||||||
uploaded_files = []
|
uploaded_files = []
|
||||||
|
|
||||||
for idx, file_path in enumerate(files, start=1):
|
for idx, file_path in enumerate(files, start=1):
|
||||||
result = upload_to_webdav.apply(
|
result = upload_to_webdav.apply(args=[file_path], kwargs={"file_id": idx + 10}).get()
|
||||||
args=[file_path],
|
|
||||||
kwargs={"file_id": idx + 10}
|
|
||||||
).get()
|
|
||||||
|
|
||||||
assert result["status"] == "Completed"
|
assert result["status"] == "Completed"
|
||||||
uploaded_files.append(os.path.basename(file_path))
|
uploaded_files.append(os.path.basename(file_path))
|
||||||
@@ -260,17 +242,16 @@ class TestWebDAVIntegration:
|
|||||||
for filename in uploaded_files:
|
for filename in uploaded_files:
|
||||||
file_url = f"{webdav_server['url']}/{filename}"
|
file_url = f"{webdav_server['url']}/{filename}"
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
file_url,
|
file_url, auth=(webdav_server["username"], webdav_server["password"]), timeout=5
|
||||||
auth=(webdav_server["username"], webdav_server["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
assert response.status_code == 200, \
|
assert response.status_code == 200, f"File {filename} not found on server"
|
||||||
f"File {filename} not found on server"
|
|
||||||
|
|
||||||
def test_overwrite_existing_file(self, webdav_server, sample_text_file, tmp_path):
|
def test_overwrite_existing_file(self, webdav_server, sample_text_file, tmp_path):
|
||||||
"""Test that uploading a file with the same name overwrites the existing one."""
|
"""Test that uploading a file with the same name overwrites the existing one."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = webdav_server["url"] + "/"
|
mock_settings.webdav_url = webdav_server["url"] + "/"
|
||||||
mock_settings.webdav_username = webdav_server["username"]
|
mock_settings.webdav_username = webdav_server["username"]
|
||||||
@@ -284,43 +265,31 @@ class TestWebDAVIntegration:
|
|||||||
file1.write_text("Original content")
|
file1.write_text("Original content")
|
||||||
|
|
||||||
# Upload first version
|
# Upload first version
|
||||||
result1 = upload_to_webdav.apply(
|
result1 = upload_to_webdav.apply(args=[str(file1)], kwargs={"file_id": 20}).get()
|
||||||
args=[str(file1)],
|
|
||||||
kwargs={"file_id": 20}
|
|
||||||
).get()
|
|
||||||
assert result1["status"] == "Completed"
|
assert result1["status"] == "Completed"
|
||||||
|
|
||||||
# Verify first version
|
# Verify first version
|
||||||
file_url = f"{webdav_server['url']}/duplicate.txt"
|
file_url = f"{webdav_server['url']}/duplicate.txt"
|
||||||
response = requests.get(
|
response = requests.get(file_url, auth=(webdav_server["username"], webdav_server["password"]), timeout=5)
|
||||||
file_url,
|
|
||||||
auth=(webdav_server["username"], webdav_server["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
assert response.text == "Original content"
|
assert response.text == "Original content"
|
||||||
|
|
||||||
# Update file content
|
# Update file content
|
||||||
file1.write_text("Updated content - version 2")
|
file1.write_text("Updated content - version 2")
|
||||||
|
|
||||||
# Upload second version
|
# Upload second version
|
||||||
result2 = upload_to_webdav.apply(
|
result2 = upload_to_webdav.apply(args=[str(file1)], kwargs={"file_id": 21}).get()
|
||||||
args=[str(file1)],
|
|
||||||
kwargs={"file_id": 21}
|
|
||||||
).get()
|
|
||||||
assert result2["status"] == "Completed"
|
assert result2["status"] == "Completed"
|
||||||
|
|
||||||
# Verify file was overwritten
|
# Verify file was overwritten
|
||||||
response = requests.get(
|
response = requests.get(file_url, auth=(webdav_server["username"], webdav_server["password"]), timeout=5)
|
||||||
file_url,
|
|
||||||
auth=(webdav_server["username"], webdav_server["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
assert response.text == "Updated content - version 2"
|
assert response.text == "Updated content - version 2"
|
||||||
|
|
||||||
def test_large_file_upload(self, webdav_server, tmp_path):
|
def test_large_file_upload(self, webdav_server, tmp_path):
|
||||||
"""Test uploading a larger file (1MB)."""
|
"""Test uploading a larger file (1MB)."""
|
||||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \
|
with (
|
||||||
patch("app.tasks.upload_to_webdav.log_task_progress"):
|
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
|
||||||
|
patch("app.tasks.upload_to_webdav.log_task_progress"),
|
||||||
|
):
|
||||||
|
|
||||||
mock_settings.webdav_url = webdav_server["url"] + "/"
|
mock_settings.webdav_url = webdav_server["url"] + "/"
|
||||||
mock_settings.webdav_username = webdav_server["username"]
|
mock_settings.webdav_username = webdav_server["username"]
|
||||||
@@ -334,24 +303,18 @@ class TestWebDAVIntegration:
|
|||||||
large_file.write_bytes(b"X" * (1024 * 1024)) # 1MB of X's
|
large_file.write_bytes(b"X" * (1024 * 1024)) # 1MB of X's
|
||||||
|
|
||||||
# Upload the large file
|
# Upload the large file
|
||||||
result = upload_to_webdav.apply(
|
result = upload_to_webdav.apply(args=[str(large_file)], kwargs={"file_id": 30}).get()
|
||||||
args=[str(large_file)],
|
|
||||||
kwargs={"file_id": 30}
|
|
||||||
).get()
|
|
||||||
|
|
||||||
assert result["status"] == "Completed"
|
assert result["status"] == "Completed"
|
||||||
|
|
||||||
# Verify file exists and has correct size
|
# Verify file exists and has correct size
|
||||||
file_url = f"{webdav_server['url']}/large_file.bin"
|
file_url = f"{webdav_server['url']}/large_file.bin"
|
||||||
response = requests.get(
|
response = requests.get(file_url, auth=(webdav_server["username"], webdav_server["password"]), timeout=10)
|
||||||
file_url,
|
|
||||||
auth=(webdav_server["username"], webdav_server["password"]),
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert len(response.content) == 1024 * 1024, \
|
assert (
|
||||||
f"File size mismatch: expected 1MB, got {len(response.content)} bytes"
|
len(response.content) == 1024 * 1024
|
||||||
|
), f"File size mismatch: expected 1MB, got {len(response.content)} bytes"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
@@ -378,7 +341,7 @@ class TestWebDAVServerVerification:
|
|||||||
"container": container,
|
"container": container,
|
||||||
"url": f"http://{host}:{port}",
|
"url": f"http://{host}:{port}",
|
||||||
"username": "admin",
|
"username": "admin",
|
||||||
"password": "admin123"
|
"password": "admin123",
|
||||||
}
|
}
|
||||||
|
|
||||||
yield server_info
|
yield server_info
|
||||||
@@ -392,9 +355,7 @@ class TestWebDAVServerVerification:
|
|||||||
|
|
||||||
# Request with auth should succeed
|
# Request with auth should succeed
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
webdav_server["url"],
|
webdav_server["url"], auth=(webdav_server["username"], webdav_server["password"]), timeout=5
|
||||||
auth=(webdav_server["username"], webdav_server["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
assert response.status_code in [200, 301, 302]
|
assert response.status_code in [200, 301, 302]
|
||||||
|
|
||||||
@@ -408,7 +369,7 @@ class TestWebDAVServerVerification:
|
|||||||
f"{webdav_server['url']}/put_test.txt",
|
f"{webdav_server['url']}/put_test.txt",
|
||||||
auth=(webdav_server["username"], webdav_server["password"]),
|
auth=(webdav_server["username"], webdav_server["password"]),
|
||||||
data=f,
|
data=f,
|
||||||
timeout=5
|
timeout=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code in [200, 201, 204]
|
assert response.status_code in [200, 201, 204]
|
||||||
@@ -416,10 +377,7 @@ class TestWebDAVServerVerification:
|
|||||||
def test_webdav_propfind_method(self, webdav_server):
|
def test_webdav_propfind_method(self, webdav_server):
|
||||||
"""Verify WebDAV server supports PROPFIND (directory listing)."""
|
"""Verify WebDAV server supports PROPFIND (directory listing)."""
|
||||||
response = requests.request(
|
response = requests.request(
|
||||||
"PROPFIND",
|
"PROPFIND", webdav_server["url"], auth=(webdav_server["username"], webdav_server["password"]), timeout=5
|
||||||
webdav_server["url"],
|
|
||||||
auth=(webdav_server["username"], webdav_server["password"]),
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# PROPFIND may return 207 Multi-Status, 200 OK, or 403 Forbidden
|
# PROPFIND may return 207 Multi-Status, 200 OK, or 403 Forbidden
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
"""Additional view tests to increase coverage."""
|
"""Additional view tests to increase coverage."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
_TEST_CREDENTIAL = "test" # noqa: S105
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
class TestWizardPost:
|
class TestWizardPost:
|
||||||
@@ -31,7 +34,12 @@ class TestWizardPost:
|
|||||||
"""Test POST wizard step 2."""
|
"""Test POST wizard step 2."""
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/setup",
|
"/setup",
|
||||||
data={"step": "2", "session_secret": "auto-generate", "admin_username": "admin", "admin_password": "test"},
|
data={
|
||||||
|
"step": "2",
|
||||||
|
"session_secret": "auto-generate",
|
||||||
|
"admin_username": "admin",
|
||||||
|
"admin_password": _TEST_CREDENTIAL,
|
||||||
|
},
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
)
|
)
|
||||||
assert response.status_code in (200, 303)
|
assert response.status_code in (200, 303)
|
||||||
|
|||||||
Reference in New Issue
Block a user