Merge pull request #266 from christianlouis/copilot/fix-test-suite-issues
fix(test): fix test suite failures from Celery task signatures, auth imports, and incorrect mocks
This commit is contained in:
+94
-89
@@ -66,105 +66,110 @@ def get_gravatar_url(email):
|
||||
return f"https://www.gravatar.com/avatar/{email_hash}?d=identicon"
|
||||
|
||||
|
||||
if AUTH_ENABLED:
|
||||
async def login(request: Request):
|
||||
"""Show login page with appropriate authentication options"""
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{
|
||||
"request": request,
|
||||
"error": request.query_params.get("error"),
|
||||
"message": request.query_params.get("message"),
|
||||
"show_oauth": OAUTH_CONFIGURED,
|
||||
"oauth_provider_name": OAUTH_PROVIDER_NAME,
|
||||
"app_version": settings.version, # Changed from app_version to version
|
||||
},
|
||||
)
|
||||
|
||||
@router.get("/login")
|
||||
async def login(request: Request):
|
||||
"""Show login page with appropriate authentication options"""
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{
|
||||
"request": request,
|
||||
"error": request.query_params.get("error"),
|
||||
"message": request.query_params.get("message"),
|
||||
"show_oauth": OAUTH_CONFIGURED,
|
||||
"oauth_provider_name": OAUTH_PROVIDER_NAME,
|
||||
"app_version": settings.version, # Changed from app_version to version
|
||||
},
|
||||
)
|
||||
|
||||
@router.get("/oauth-login")
|
||||
async def oauth_login(request: Request):
|
||||
"""Handle OAuth login flow"""
|
||||
if not OAUTH_CONFIGURED:
|
||||
return RedirectResponse(url="/login?error=OAuth+not+configured", status_code=status.HTTP_302_FOUND)
|
||||
async def oauth_login(request: Request):
|
||||
"""Handle OAuth login flow"""
|
||||
if not OAUTH_CONFIGURED:
|
||||
return RedirectResponse(url="/login?error=OAuth+not+configured", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
redirect_uri = request.url_for("oauth_callback")
|
||||
return await oauth.authentik.authorize_redirect(request, redirect_uri)
|
||||
redirect_uri = request.url_for("oauth_callback")
|
||||
return await oauth.authentik.authorize_redirect(request, redirect_uri)
|
||||
|
||||
@router.get("/oauth-callback")
|
||||
async def oauth_callback(request: Request):
|
||||
"""Handle OAuth callback from provider"""
|
||||
try:
|
||||
token = await oauth.authentik.authorize_access_token(request)
|
||||
userinfo = token.get("userinfo")
|
||||
if not userinfo:
|
||||
return RedirectResponse(
|
||||
url="/login?error=Failed+to+retrieve+user+information", status_code=status.HTTP_302_FOUND
|
||||
)
|
||||
|
||||
# Store user info in session
|
||||
user_data = dict(userinfo)
|
||||
|
||||
# Add Gravatar picture if no picture is provided
|
||||
if not user_data.get("picture") and user_data.get("email"):
|
||||
user_data["picture"] = get_gravatar_url(user_data["email"])
|
||||
|
||||
# Check if user is admin based on OAuth groups or specific email
|
||||
# You can customize this logic based on your OAuth provider's attributes
|
||||
# For example, check if user has an "admin" group or specific email domain
|
||||
is_admin = False
|
||||
if "groups" in user_data:
|
||||
# Check if user is in admin group
|
||||
groups = user_data.get("groups", [])
|
||||
admin_group = (settings.admin_group_name or "admin").strip().lower()
|
||||
is_admin = admin_group in [group.lower() for group in groups]
|
||||
|
||||
# Set is_admin flag (defaults to False for OAuth users unless they're in admin group)
|
||||
user_data["is_admin"] = is_admin
|
||||
|
||||
request.session["user"] = user_data
|
||||
|
||||
# Log the successful authentication
|
||||
print(f"User authenticated via OAuth: {user_data.get('email', 'No email')} (admin: {is_admin})")
|
||||
|
||||
# Redirect to original destination or default
|
||||
redirect_url = request.session.pop("redirect_after_login", "/upload")
|
||||
return RedirectResponse(url=redirect_url)
|
||||
except Exception as e:
|
||||
print(f"OAuth authentication error: {str(e)}")
|
||||
async def oauth_callback(request: Request):
|
||||
"""Handle OAuth callback from provider"""
|
||||
try:
|
||||
token = await oauth.authentik.authorize_access_token(request)
|
||||
userinfo = token.get("userinfo")
|
||||
if not userinfo:
|
||||
return RedirectResponse(
|
||||
url=f"/login?error=Authentication+failed:+{str(e)}", status_code=status.HTTP_302_FOUND
|
||||
url="/login?error=Failed+to+retrieve+user+information", status_code=status.HTTP_302_FOUND
|
||||
)
|
||||
|
||||
@router.post("/auth")
|
||||
async def auth(request: Request):
|
||||
"""Handle local username/password authentication"""
|
||||
form_data = await request.form()
|
||||
username = form_data.get("username")
|
||||
password = form_data.get("password")
|
||||
# Store user info in session
|
||||
user_data = dict(userinfo)
|
||||
|
||||
if username == settings.admin_username and password == settings.admin_password:
|
||||
# Create user session
|
||||
request.session["user"] = {
|
||||
"id": "admin",
|
||||
"name": "Administrator",
|
||||
"email": f"{username}@local.docuelevate",
|
||||
"preferred_username": username,
|
||||
"picture": "/static/images/default-avatar.svg",
|
||||
"is_admin": True,
|
||||
}
|
||||
# Redirect to original destination or default
|
||||
redirect_url = request.session.pop("redirect_after_login", "/upload")
|
||||
return RedirectResponse(url=redirect_url, status_code=302)
|
||||
else:
|
||||
return RedirectResponse(url="/login?error=Invalid+username+or+password", status_code=302)
|
||||
# Add Gravatar picture if no picture is provided
|
||||
if not user_data.get("picture") and user_data.get("email"):
|
||||
user_data["picture"] = get_gravatar_url(user_data["email"])
|
||||
|
||||
@router.get("/logout")
|
||||
async def logout(request: Request):
|
||||
"""Handle user logout"""
|
||||
request.session.pop("user", None)
|
||||
return RedirectResponse(url="/login?message=You+have+been+logged+out+successfully", status_code=302)
|
||||
# Check if user is admin based on OAuth groups or specific email
|
||||
# You can customize this logic based on your OAuth provider's attributes
|
||||
# For example, check if user has an "admin" group or specific email domain
|
||||
is_admin = False
|
||||
if "groups" in user_data:
|
||||
# Check if user is in admin group
|
||||
groups = user_data.get("groups", [])
|
||||
admin_group = (settings.admin_group_name or "admin").strip().lower()
|
||||
is_admin = admin_group in [group.lower() for group in groups]
|
||||
|
||||
# Set is_admin flag (defaults to False for OAuth users unless they're in admin group)
|
||||
user_data["is_admin"] = is_admin
|
||||
|
||||
request.session["user"] = user_data
|
||||
|
||||
# Log the successful authentication
|
||||
print(f"User authenticated via OAuth: {user_data.get('email', 'No email')} (admin: {is_admin})")
|
||||
|
||||
# Redirect to original destination or default
|
||||
redirect_url = request.session.pop("redirect_after_login", "/upload")
|
||||
return RedirectResponse(url=redirect_url, status_code=status.HTTP_302_FOUND)
|
||||
except Exception as e:
|
||||
print(f"OAuth authentication error: {str(e)}")
|
||||
return RedirectResponse(
|
||||
url=f"/login?error=Authentication+failed:+{str(e)}", status_code=status.HTTP_302_FOUND
|
||||
)
|
||||
|
||||
|
||||
async def auth(request: Request):
|
||||
"""Handle local username/password authentication"""
|
||||
form_data = await request.form()
|
||||
username = form_data.get("username")
|
||||
password = form_data.get("password")
|
||||
|
||||
if username == settings.admin_username and password == settings.admin_password:
|
||||
# Create user session
|
||||
request.session["user"] = {
|
||||
"id": "admin",
|
||||
"name": "Administrator",
|
||||
"email": f"{username}@local.docuelevate",
|
||||
"preferred_username": username,
|
||||
"picture": "/static/images/default-avatar.svg",
|
||||
"is_admin": True,
|
||||
}
|
||||
# Redirect to original destination or default
|
||||
redirect_url = request.session.pop("redirect_after_login", "/upload")
|
||||
return RedirectResponse(url=redirect_url, status_code=302)
|
||||
else:
|
||||
return RedirectResponse(url="/login?error=Invalid+username+or+password", status_code=302)
|
||||
|
||||
|
||||
async def logout(request: Request):
|
||||
"""Handle user logout"""
|
||||
request.session.pop("user", None)
|
||||
return RedirectResponse(url="/login?message=You+have+been+logged+out+successfully", status_code=302)
|
||||
|
||||
|
||||
if AUTH_ENABLED:
|
||||
router.add_api_route("/login", login, methods=["GET"])
|
||||
router.add_api_route("/oauth-login", oauth_login, methods=["GET"])
|
||||
router.add_api_route("/oauth-callback", oauth_callback, methods=["GET"])
|
||||
router.add_api_route("/auth", auth, methods=["POST"])
|
||||
router.add_api_route("/logout", logout, methods=["GET"])
|
||||
|
||||
|
||||
@router.get("/api/auth/whoami")
|
||||
|
||||
+45
-50
@@ -182,57 +182,52 @@ def oauth_enabled_app(oauth_config: Dict[str, str]):
|
||||
Configured test client
|
||||
"""
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
# Save original values
|
||||
original_auth_enabled = os.environ.get("AUTH_ENABLED")
|
||||
original_client_id = os.environ.get("AUTHENTIK_CLIENT_ID")
|
||||
original_client_secret = os.environ.get("AUTHENTIK_CLIENT_SECRET")
|
||||
original_config_url = os.environ.get("AUTHENTIK_CONFIG_URL")
|
||||
|
||||
|
||||
from app.main import app
|
||||
import app.auth as auth_module
|
||||
from app.auth import login, oauth_login, oauth_callback, auth, logout
|
||||
|
||||
# Save original state
|
||||
original_auth_enabled = auth_module.AUTH_ENABLED
|
||||
original_oauth_configured = auth_module.OAUTH_CONFIGURED
|
||||
original_oauth_provider = auth_module.OAUTH_PROVIDER_NAME
|
||||
original_route_count = len(app.router.routes)
|
||||
|
||||
try:
|
||||
# Enable auth and configure OAuth
|
||||
os.environ["AUTH_ENABLED"] = "True"
|
||||
os.environ["AUTHENTIK_CLIENT_ID"] = oauth_config["client_id"]
|
||||
os.environ["AUTHENTIK_CLIENT_SECRET"] = oauth_config["client_secret"]
|
||||
os.environ["AUTHENTIK_CONFIG_URL"] = oauth_config["server_metadata_url"]
|
||||
|
||||
# Need to reload the app module to pick up new config
|
||||
import importlib
|
||||
from app import auth
|
||||
importlib.reload(auth)
|
||||
|
||||
# Enable auth and configure OAuth flags
|
||||
auth_module.AUTH_ENABLED = True
|
||||
auth_module.OAUTH_CONFIGURED = True
|
||||
auth_module.OAUTH_PROVIDER_NAME = oauth_config.get("provider_name", "Test SSO")
|
||||
|
||||
# Register OAuth client
|
||||
auth_module.oauth.register(
|
||||
name="authentik",
|
||||
client_id=oauth_config["client_id"],
|
||||
client_secret=oauth_config["client_secret"],
|
||||
server_metadata_url=oauth_config["server_metadata_url"],
|
||||
client_kwargs={"scope": "openid profile email"},
|
||||
)
|
||||
|
||||
# Add auth routes directly to the app (since include_router was called at startup
|
||||
# with AUTH_ENABLED=False, routes weren't registered)
|
||||
app.add_api_route("/login", login, methods=["GET"])
|
||||
app.add_api_route("/oauth-login", oauth_login, methods=["GET"])
|
||||
app.add_api_route("/oauth-callback", oauth_callback, methods=["GET"], name="oauth_callback")
|
||||
app.add_api_route("/auth", auth, methods=["POST"])
|
||||
app.add_api_route("/logout", logout, methods=["GET"])
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
|
||||
# Create test client
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
# Create test client with base_url to satisfy TrustedHostMiddleware
|
||||
client = TestClient(app, base_url="http://localhost")
|
||||
|
||||
yield client
|
||||
|
||||
|
||||
finally:
|
||||
# Restore original values
|
||||
if original_auth_enabled is not None:
|
||||
os.environ["AUTH_ENABLED"] = original_auth_enabled
|
||||
else:
|
||||
os.environ.pop("AUTH_ENABLED", None)
|
||||
|
||||
if original_client_id is not None:
|
||||
os.environ["AUTHENTIK_CLIENT_ID"] = original_client_id
|
||||
else:
|
||||
os.environ.pop("AUTHENTIK_CLIENT_ID", None)
|
||||
|
||||
if original_client_secret is not None:
|
||||
os.environ["AUTHENTIK_CLIENT_SECRET"] = original_client_secret
|
||||
else:
|
||||
os.environ.pop("AUTHENTIK_CLIENT_SECRET", None)
|
||||
|
||||
if original_config_url is not None:
|
||||
os.environ["AUTHENTIK_CONFIG_URL"] = original_config_url
|
||||
else:
|
||||
os.environ.pop("AUTHENTIK_CONFIG_URL", None)
|
||||
|
||||
# Reload auth module to restore original state
|
||||
import importlib
|
||||
from app import auth
|
||||
importlib.reload(auth)
|
||||
# Restore original auth state
|
||||
auth_module.AUTH_ENABLED = original_auth_enabled
|
||||
auth_module.OAUTH_CONFIGURED = original_oauth_configured
|
||||
auth_module.OAUTH_PROVIDER_NAME = original_oauth_provider
|
||||
|
||||
# Remove added routes
|
||||
app.router.routes = app.router.routes[:original_route_count]
|
||||
|
||||
+23
-24
@@ -163,7 +163,7 @@ class TestRequireLogin:
|
||||
mock_request.session = {"user": {"id": "123"}}
|
||||
mock_request.url = "http://localhost/test"
|
||||
|
||||
result = test_sync_endpoint(mock_request)
|
||||
result = await test_sync_endpoint(mock_request)
|
||||
|
||||
assert result == {"message": "success"}
|
||||
|
||||
@@ -180,7 +180,7 @@ class TestRequireLogin:
|
||||
mock_request.session = {}
|
||||
mock_request.url = "http://localhost/protected"
|
||||
|
||||
result = test_sync_endpoint(mock_request)
|
||||
result = await test_sync_endpoint(mock_request)
|
||||
|
||||
assert isinstance(result, RedirectResponse)
|
||||
assert result.status_code == status.HTTP_302_FOUND
|
||||
@@ -192,43 +192,42 @@ class TestOAuthConfiguration:
|
||||
|
||||
def test_oauth_not_configured_without_credentials(self):
|
||||
"""Test OAuth is not configured when credentials are missing."""
|
||||
with patch("app.auth.settings") as mock_settings:
|
||||
mock_settings.auth_enabled = True
|
||||
mock_settings.authentik_client_id = None
|
||||
mock_settings.authentik_client_secret = None
|
||||
import importlib
|
||||
|
||||
# Re-import to trigger configuration logic
|
||||
import importlib
|
||||
import app.auth
|
||||
|
||||
import app.auth
|
||||
try:
|
||||
with patch("app.config.settings") as mock_settings:
|
||||
mock_settings.auth_enabled = True
|
||||
mock_settings.authentik_client_id = None
|
||||
mock_settings.authentik_client_secret = None
|
||||
|
||||
importlib.reload(app.auth)
|
||||
|
||||
assert app.auth.OAUTH_CONFIGURED is False
|
||||
finally:
|
||||
importlib.reload(app.auth)
|
||||
|
||||
from app.auth import OAUTH_CONFIGURED
|
||||
|
||||
assert OAUTH_CONFIGURED is False
|
||||
|
||||
def test_oauth_configured_with_credentials(self):
|
||||
"""Test OAuth is configured when credentials are provided."""
|
||||
with patch("app.auth.settings") as mock_settings:
|
||||
with patch("app.auth.oauth") as mock_oauth:
|
||||
import importlib
|
||||
|
||||
import app.auth
|
||||
|
||||
try:
|
||||
with patch("app.config.settings") as mock_settings:
|
||||
mock_settings.auth_enabled = True
|
||||
mock_settings.authentik_client_id = "test_client_id"
|
||||
mock_settings.authentik_client_secret = "test_secret"
|
||||
mock_settings.authentik_config_url = "https://auth.example.com/.well-known/openid-configuration"
|
||||
mock_settings.oauth_provider_name = "Test SSO"
|
||||
|
||||
# Re-import to trigger configuration logic
|
||||
import importlib
|
||||
|
||||
import app.auth
|
||||
|
||||
importlib.reload(app.auth)
|
||||
|
||||
from app.auth import OAUTH_CONFIGURED, OAUTH_PROVIDER_NAME
|
||||
|
||||
assert OAUTH_CONFIGURED is True
|
||||
assert OAUTH_PROVIDER_NAME == "Test SSO"
|
||||
assert app.auth.OAUTH_CONFIGURED is True
|
||||
assert app.auth.OAUTH_PROVIDER_NAME == "Test SSO"
|
||||
finally:
|
||||
importlib.reload(app.auth)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
||||
+18
-37
@@ -178,10 +178,6 @@ class TestConvertToPdf:
|
||||
mock_response.content = b"%PDF-1.4 converted content"
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
# Mock task context
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
|
||||
# Mock file type detection
|
||||
with patch("app.tasks.convert_to_pdf._detect_mime_type") as mock_detect_mime:
|
||||
with patch("app.tasks.convert_to_pdf._detect_extension") as mock_detect_ext:
|
||||
@@ -194,7 +190,8 @@ class TestConvertToPdf:
|
||||
)
|
||||
mock_detect_ext.return_value = ".docx"
|
||||
|
||||
result = convert_to_pdf.__wrapped__(mock_task, "/tmp/test.docx", "document.docx")
|
||||
convert_to_pdf.request.id = "test-task-id"
|
||||
result = convert_to_pdf.__wrapped__("/tmp/test.docx", "document.docx")
|
||||
|
||||
# Verify Gotenberg was called
|
||||
mock_post.assert_called_once()
|
||||
@@ -214,13 +211,11 @@ class TestConvertToPdf:
|
||||
@patch("app.tasks.convert_to_pdf.log_task_progress")
|
||||
def test_returns_none_when_gotenberg_url_not_configured(self, mock_log_progress):
|
||||
"""Test returns None when Gotenberg URL is not configured."""
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
|
||||
with patch("app.tasks.convert_to_pdf.settings") as mock_settings:
|
||||
mock_settings.gotenberg_url = None
|
||||
|
||||
result = convert_to_pdf.__wrapped__(mock_task, "/tmp/test.docx")
|
||||
convert_to_pdf.request.id = "test-task-id"
|
||||
result = convert_to_pdf.__wrapped__("/tmp/test.docx")
|
||||
|
||||
assert result is None
|
||||
# Verify error was logged
|
||||
@@ -230,9 +225,6 @@ class TestConvertToPdf:
|
||||
@patch("app.tasks.convert_to_pdf.log_task_progress")
|
||||
def test_returns_none_when_file_type_unknown(self, mock_log_progress):
|
||||
"""Test returns None when file type cannot be determined."""
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
|
||||
with patch("app.tasks.convert_to_pdf._detect_mime_type") as mock_detect_mime:
|
||||
with patch("app.tasks.convert_to_pdf._detect_extension") as mock_detect_ext:
|
||||
with patch("app.tasks.convert_to_pdf.settings") as mock_settings:
|
||||
@@ -240,7 +232,8 @@ class TestConvertToPdf:
|
||||
mock_detect_mime.return_value = (None, None)
|
||||
mock_detect_ext.return_value = ""
|
||||
|
||||
result = convert_to_pdf.__wrapped__(mock_task, "/tmp/unknown_file")
|
||||
convert_to_pdf.request.id = "test-task-id"
|
||||
result = convert_to_pdf.__wrapped__("/tmp/unknown_file")
|
||||
|
||||
assert result is None
|
||||
|
||||
@@ -255,9 +248,6 @@ class TestConvertToPdf:
|
||||
mock_response.content = b"%PDF-1.4 converted image"
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
|
||||
with patch("app.tasks.convert_to_pdf._detect_mime_type") as mock_detect_mime:
|
||||
with patch("app.tasks.convert_to_pdf._detect_extension") as mock_detect_ext:
|
||||
with patch("app.tasks.convert_to_pdf.settings") as mock_settings:
|
||||
@@ -266,7 +256,8 @@ class TestConvertToPdf:
|
||||
mock_detect_mime.return_value = ("image/jpeg", None)
|
||||
mock_detect_ext.return_value = ".jpg"
|
||||
|
||||
result = convert_to_pdf.__wrapped__(mock_task, "/tmp/photo.jpg")
|
||||
convert_to_pdf.request.id = "test-task-id"
|
||||
result = convert_to_pdf.__wrapped__("/tmp/photo.jpg")
|
||||
|
||||
# Verify LibreOffice endpoint was used for images
|
||||
mock_post.assert_called_once()
|
||||
@@ -285,9 +276,6 @@ class TestConvertToPdf:
|
||||
mock_response.content = b"%PDF-1.4 converted html"
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
|
||||
with patch("app.tasks.convert_to_pdf._detect_mime_type") as mock_detect_mime:
|
||||
with patch("app.tasks.convert_to_pdf._detect_extension") as mock_detect_ext:
|
||||
with patch("app.tasks.convert_to_pdf.settings") as mock_settings:
|
||||
@@ -296,7 +284,8 @@ class TestConvertToPdf:
|
||||
mock_detect_mime.return_value = ("text/html", None)
|
||||
mock_detect_ext.return_value = ".html"
|
||||
|
||||
result = convert_to_pdf.__wrapped__(mock_task, "/tmp/page.html")
|
||||
convert_to_pdf.request.id = "test-task-id"
|
||||
result = convert_to_pdf.__wrapped__("/tmp/page.html")
|
||||
|
||||
# Verify Chromium endpoint was used
|
||||
mock_post.assert_called_once()
|
||||
@@ -314,9 +303,6 @@ class TestConvertToPdf:
|
||||
mock_response.content = b"%PDF-1.4 converted markdown"
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
|
||||
with patch("app.tasks.convert_to_pdf._detect_mime_type") as mock_detect_mime:
|
||||
with patch("app.tasks.convert_to_pdf._detect_extension") as mock_detect_ext:
|
||||
with patch("app.tasks.convert_to_pdf.settings") as mock_settings:
|
||||
@@ -330,7 +316,8 @@ class TestConvertToPdf:
|
||||
mock_exists.return_value = True
|
||||
mock_dirname.return_value = "/tmp"
|
||||
|
||||
result = convert_to_pdf.__wrapped__(mock_task, "/tmp/readme.md")
|
||||
convert_to_pdf.request.id = "test-task-id"
|
||||
result = convert_to_pdf.__wrapped__("/tmp/readme.md")
|
||||
|
||||
# Verify Chromium markdown endpoint was used
|
||||
mock_post.assert_called_once()
|
||||
@@ -347,9 +334,6 @@ class TestConvertToPdf:
|
||||
mock_response.text = "Internal Server Error"
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
|
||||
with patch("app.tasks.convert_to_pdf._detect_mime_type") as mock_detect_mime:
|
||||
with patch("app.tasks.convert_to_pdf._detect_extension") as mock_detect_ext:
|
||||
with patch("app.tasks.convert_to_pdf.settings") as mock_settings:
|
||||
@@ -358,7 +342,8 @@ class TestConvertToPdf:
|
||||
mock_detect_mime.return_value = ("application/pdf", None)
|
||||
mock_detect_ext.return_value = ".pdf"
|
||||
|
||||
result = convert_to_pdf.__wrapped__(mock_task, "/tmp/test.pdf")
|
||||
convert_to_pdf.request.id = "test-task-id"
|
||||
result = convert_to_pdf.__wrapped__("/tmp/test.pdf")
|
||||
|
||||
assert result is None
|
||||
# Verify error was logged
|
||||
@@ -372,9 +357,6 @@ class TestConvertToPdf:
|
||||
"""Test handling of network exceptions during conversion."""
|
||||
mock_post.side_effect = Exception("Connection timeout")
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
|
||||
with patch("app.tasks.convert_to_pdf._detect_mime_type") as mock_detect_mime:
|
||||
with patch("app.tasks.convert_to_pdf._detect_extension") as mock_detect_ext:
|
||||
with patch("app.tasks.convert_to_pdf.settings") as mock_settings:
|
||||
@@ -383,7 +365,8 @@ class TestConvertToPdf:
|
||||
mock_detect_mime.return_value = ("application/pdf", None)
|
||||
mock_detect_ext.return_value = ".pdf"
|
||||
|
||||
result = convert_to_pdf.__wrapped__(mock_task, "/tmp/test.pdf")
|
||||
convert_to_pdf.request.id = "test-task-id"
|
||||
result = convert_to_pdf.__wrapped__("/tmp/test.pdf")
|
||||
|
||||
assert result is None
|
||||
|
||||
@@ -398,9 +381,6 @@ class TestConvertToPdf:
|
||||
mock_response.content = b"%PDF-1.4"
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
|
||||
with patch("app.tasks.convert_to_pdf._detect_mime_type") as mock_detect_mime:
|
||||
with patch("app.tasks.convert_to_pdf._detect_extension") as mock_detect_ext:
|
||||
with patch("app.tasks.convert_to_pdf.settings") as mock_settings:
|
||||
@@ -409,7 +389,8 @@ class TestConvertToPdf:
|
||||
mock_detect_mime.return_value = ("application/vnd.ms-excel", None)
|
||||
mock_detect_ext.return_value = ".xls"
|
||||
|
||||
result = convert_to_pdf.__wrapped__(mock_task, "/tmp/uuid.xls", "report.xls")
|
||||
convert_to_pdf.request.id = "test-task-id"
|
||||
result = convert_to_pdf.__wrapped__("/tmp/uuid.xls", "report.xls")
|
||||
|
||||
# Verify process_document was called with modified original filename
|
||||
mock_process.delay.assert_called_once()
|
||||
|
||||
@@ -125,9 +125,7 @@ class TestEmbedMetadataIntoPdf:
|
||||
with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings:
|
||||
mock_settings.workdir = "/workdir"
|
||||
|
||||
# Mock task context
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
embed_metadata_into_pdf.request.id = "test-task-id"
|
||||
|
||||
metadata = {
|
||||
"filename": "2024-01-15_Invoice.pdf",
|
||||
@@ -137,7 +135,7 @@ class TestEmbedMetadataIntoPdf:
|
||||
}
|
||||
|
||||
result = embed_metadata_into_pdf.__wrapped__(
|
||||
mock_task, "/workdir/tmp/test.pdf", "Sample text", metadata, file_id=123
|
||||
"/workdir/tmp/test.pdf", "Sample text", metadata, file_id=123
|
||||
)
|
||||
|
||||
# Verify PDF metadata was set
|
||||
@@ -167,11 +165,10 @@ class TestEmbedMetadataIntoPdf:
|
||||
with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings:
|
||||
mock_settings.workdir = "/workdir"
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
embed_metadata_into_pdf.request.id = "test-task-id"
|
||||
|
||||
result = embed_metadata_into_pdf.__wrapped__(
|
||||
mock_task, "/nonexistent/file.pdf", "text", {"filename": "test.pdf"}, file_id=123
|
||||
"/nonexistent/file.pdf", "text", {"filename": "test.pdf"}, file_id=123
|
||||
)
|
||||
|
||||
assert result == {"error": "File not found"}
|
||||
@@ -220,11 +217,10 @@ class TestEmbedMetadataIntoPdf:
|
||||
with patch("app.tasks.embed_metadata_into_pdf.tempfile.NamedTemporaryFile"):
|
||||
mock_settings.workdir = "/workdir"
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
embed_metadata_into_pdf.request.id = "test-task-id"
|
||||
|
||||
result = embed_metadata_into_pdf.__wrapped__(
|
||||
mock_task, "/workdir/tmp/test.pdf", "text", {"filename": "test.pdf"}
|
||||
"/workdir/tmp/test.pdf", "text", {"filename": "test.pdf"}
|
||||
)
|
||||
|
||||
# Verify database was queried
|
||||
@@ -253,11 +249,10 @@ class TestEmbedMetadataIntoPdf:
|
||||
with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings:
|
||||
mock_settings.workdir = "/workdir"
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
embed_metadata_into_pdf.request.id = "test-task-id"
|
||||
|
||||
result = embed_metadata_into_pdf.__wrapped__(
|
||||
mock_task, "/workdir/tmp/test.pdf", "text", {"filename": "test.pdf"}, file_id=789
|
||||
"/workdir/tmp/test.pdf", "text", {"filename": "test.pdf"}, file_id=789
|
||||
)
|
||||
|
||||
assert "error" in result
|
||||
@@ -318,14 +313,13 @@ class TestEmbedMetadataIntoPdf:
|
||||
with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings:
|
||||
mock_settings.workdir = "/workdir"
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
embed_metadata_into_pdf.request.id = "test-task-id"
|
||||
|
||||
# Try to embed metadata with malicious filename
|
||||
metadata = {"filename": "../../../etc/passwd"}
|
||||
|
||||
result = embed_metadata_into_pdf.__wrapped__(
|
||||
mock_task, "/workdir/tmp/test.pdf", "text", metadata, file_id=111
|
||||
"/workdir/tmp/test.pdf", "text", metadata, file_id=111
|
||||
)
|
||||
|
||||
# Verify sanitize_filename was called
|
||||
@@ -383,14 +377,13 @@ class TestEmbedMetadataIntoPdf:
|
||||
with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings:
|
||||
mock_settings.workdir = "/workdir"
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
embed_metadata_into_pdf.request.id = "test-task-id"
|
||||
|
||||
# Metadata with missing fields
|
||||
metadata = {}
|
||||
|
||||
result = embed_metadata_into_pdf.__wrapped__(
|
||||
mock_task, "/workdir/tmp/test.pdf", "text", metadata, file_id=222
|
||||
"/workdir/tmp/test.pdf", "text", metadata, file_id=222
|
||||
)
|
||||
|
||||
# Verify PDF metadata was set with defaults
|
||||
@@ -450,21 +443,22 @@ class TestEmbedMetadataIntoPdf:
|
||||
with patch("app.tasks.embed_metadata_into_pdf.Path") as mock_path_class:
|
||||
# Mock Path for deletion logic
|
||||
mock_original_path = MagicMock()
|
||||
mock_original_path.exists.return_value = True
|
||||
mock_original_path.is_relative_to.return_value = True
|
||||
mock_resolved_path = MagicMock()
|
||||
mock_resolved_path.exists.return_value = True
|
||||
mock_resolved_path.is_relative_to.return_value = True
|
||||
mock_original_path.resolve.return_value = mock_resolved_path
|
||||
mock_workdir_path = MagicMock()
|
||||
mock_path_class.side_effect = [mock_workdir_path, mock_original_path, mock_workdir_path]
|
||||
mock_path_class.side_effect = [mock_workdir_path, mock_original_path]
|
||||
|
||||
with patch("app.tasks.embed_metadata_into_pdf.os.remove"):
|
||||
with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings:
|
||||
mock_settings.workdir = "/workdir"
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
embed_metadata_into_pdf.request.id = "test-task-id"
|
||||
|
||||
result = embed_metadata_into_pdf.__wrapped__(
|
||||
mock_task, "/workdir/tmp/test.pdf", "text", {"filename": "test.pdf"}, file_id=333
|
||||
"/workdir/tmp/test.pdf", "text", {"filename": "test.pdf"}, file_id=333
|
||||
)
|
||||
|
||||
# Verify unlink (delete) was called
|
||||
mock_original_path.unlink.assert_called_once()
|
||||
# Verify unlink (delete) was called on the resolved path
|
||||
mock_resolved_path.unlink.assert_called_once()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Comprehensive unit tests for app/tasks/extract_metadata_with_gpt.py module."""
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -89,12 +89,11 @@ class TestExtractMetadataWithGpt:
|
||||
})
|
||||
mock_client.chat.completions.create.return_value = mock_completion
|
||||
|
||||
# Mock the task context
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
# Set task request context directly on the Celery task
|
||||
extract_metadata_with_gpt.request.id = "test-task-id"
|
||||
|
||||
# Call the underlying function directly (not through Celery)
|
||||
result = extract_metadata_with_gpt.__wrapped__(mock_task, "test_invoice.pdf", "Invoice from Amazon for 99.99 EUR", 123)
|
||||
result = extract_metadata_with_gpt.__wrapped__("test_invoice.pdf", "Invoice from Amazon for 99.99 EUR", 123)
|
||||
|
||||
# Verify OpenAI was called
|
||||
mock_client.chat.completions.create.assert_called_once()
|
||||
@@ -123,10 +122,9 @@ class TestExtractMetadataWithGpt:
|
||||
mock_completion.choices[0].message.content = '```json\n{"filename": "test.pdf", "document_type": "Unknown"}\n```'
|
||||
mock_client.chat.completions.create.return_value = mock_completion
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
extract_metadata_with_gpt.request.id = "test-task-id"
|
||||
|
||||
result = extract_metadata_with_gpt.__wrapped__(mock_task, "test.pdf", "Sample text", 456)
|
||||
result = extract_metadata_with_gpt.__wrapped__("test.pdf", "Sample text", 456)
|
||||
|
||||
assert result["metadata"]["filename"] == "test.pdf"
|
||||
assert result["metadata"]["document_type"] == "Unknown"
|
||||
@@ -141,10 +139,9 @@ class TestExtractMetadataWithGpt:
|
||||
mock_completion.choices[0].message.content = "This is not valid JSON at all"
|
||||
mock_client.chat.completions.create.return_value = mock_completion
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
extract_metadata_with_gpt.request.id = "test-task-id"
|
||||
|
||||
result = extract_metadata_with_gpt.__wrapped__(mock_task, "test.pdf", "Sample text", 789)
|
||||
result = extract_metadata_with_gpt.__wrapped__("test.pdf", "Sample text", 789)
|
||||
|
||||
assert result == {}
|
||||
mock_embed_task.delay.assert_not_called()
|
||||
@@ -159,10 +156,9 @@ class TestExtractMetadataWithGpt:
|
||||
"""Test handling of OpenAI API exceptions."""
|
||||
mock_client.chat.completions.create.side_effect = Exception("API Error: Rate limit exceeded")
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
extract_metadata_with_gpt.request.id = "test-task-id"
|
||||
|
||||
result = extract_metadata_with_gpt.__wrapped__(mock_task, "test.pdf", "Sample text", 101)
|
||||
result = extract_metadata_with_gpt.__wrapped__("test.pdf", "Sample text", 101)
|
||||
|
||||
assert result == {}
|
||||
mock_embed_task.delay.assert_not_called()
|
||||
@@ -192,11 +188,9 @@ class TestExtractMetadataWithGpt:
|
||||
# Mock file existence
|
||||
with patch("app.tasks.extract_metadata_with_gpt.os.path.exists", return_value=True):
|
||||
with patch("app.tasks.extract_metadata_with_gpt.settings.workdir", "/tmp"):
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
extract_metadata_with_gpt.request.id = "test-task-id"
|
||||
|
||||
result = extract_metadata_with_gpt.__wrapped__(
|
||||
mock_task,
|
||||
filename="test.pdf",
|
||||
cleaned_text="Sample text",
|
||||
file_id=None # Not provided
|
||||
@@ -219,10 +213,9 @@ class TestExtractMetadataWithGpt:
|
||||
})
|
||||
mock_client.chat.completions.create.return_value = mock_completion
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
extract_metadata_with_gpt.request.id = "test-task-id"
|
||||
|
||||
result = extract_metadata_with_gpt.__wrapped__(mock_task, "test.pdf", "Sample text", 202)
|
||||
result = extract_metadata_with_gpt.__wrapped__("test.pdf", "Sample text", 202)
|
||||
|
||||
# Filename should be sanitized (empty or safe)
|
||||
assert result["metadata"]["filename"] == ""
|
||||
@@ -240,10 +233,9 @@ class TestExtractMetadataWithGpt:
|
||||
})
|
||||
mock_client.chat.completions.create.return_value = mock_completion
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
extract_metadata_with_gpt.request.id = "test-task-id"
|
||||
|
||||
result = extract_metadata_with_gpt.__wrapped__(mock_task, "test.pdf", "Sample text", 303)
|
||||
result = extract_metadata_with_gpt.__wrapped__("test.pdf", "Sample text", 303)
|
||||
|
||||
# Filename with .. should be rejected
|
||||
assert result["metadata"]["filename"] == ""
|
||||
@@ -260,10 +252,9 @@ class TestExtractMetadataWithGpt:
|
||||
})
|
||||
mock_client.chat.completions.create.return_value = mock_completion
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
extract_metadata_with_gpt.request.id = "test-task-id"
|
||||
|
||||
result = extract_metadata_with_gpt.__wrapped__(mock_task, "test.pdf", "Sample text", 404)
|
||||
result = extract_metadata_with_gpt.__wrapped__("test.pdf", "Sample text", 404)
|
||||
|
||||
# Valid filename should be preserved
|
||||
assert result["metadata"]["filename"] == "2024-01-15_Invoice_Amazon.pdf"
|
||||
@@ -277,10 +268,9 @@ class TestExtractMetadataWithGpt:
|
||||
mock_completion.choices[0].message.content = '{"unexpected_field": "value"}'
|
||||
mock_client.chat.completions.create.return_value = mock_completion
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
extract_metadata_with_gpt.request.id = "test-task-id"
|
||||
|
||||
result = extract_metadata_with_gpt.__wrapped__(mock_task, "test.pdf", "Sample text", 505)
|
||||
result = extract_metadata_with_gpt.__wrapped__("test.pdf", "Sample text", 505)
|
||||
|
||||
# Should still extract the JSON even if fields are unexpected
|
||||
assert "metadata" in result
|
||||
|
||||
@@ -44,8 +44,7 @@ class TestFinalizeDocumentStorage:
|
||||
with patch("app.tasks.finalize_document_storage.os.path.exists", return_value=True):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.getsize", return_value=102400):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.basename", return_value="test_document.pdf"):
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
finalize_document_storage.request.id = "test-task-id"
|
||||
|
||||
metadata = {
|
||||
"filename": "test_document.pdf",
|
||||
@@ -54,7 +53,6 @@ class TestFinalizeDocumentStorage:
|
||||
}
|
||||
|
||||
result = finalize_document_storage.__wrapped__(
|
||||
mock_task,
|
||||
original_file="/tmp/original.pdf",
|
||||
processed_file="/workdir/processed/test_document.pdf",
|
||||
metadata=metadata,
|
||||
@@ -108,11 +106,9 @@ class TestFinalizeDocumentStorage:
|
||||
with patch("app.tasks.finalize_document_storage.settings") as mock_settings:
|
||||
mock_settings.workdir = "/tmp"
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
finalize_document_storage.request.id = "test-task-id"
|
||||
|
||||
result = finalize_document_storage.__wrapped__(
|
||||
mock_task,
|
||||
original_file="/tmp/original.pdf",
|
||||
processed_file="/workdir/processed/doc.pdf",
|
||||
metadata={"filename": "doc.pdf"},
|
||||
@@ -154,11 +150,9 @@ class TestFinalizeDocumentStorage:
|
||||
with patch("app.tasks.finalize_document_storage.os.path.exists", return_value=True):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.getsize", return_value=1024):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.basename", return_value="test.pdf"):
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
finalize_document_storage.request.id = "test-task-id"
|
||||
|
||||
result = finalize_document_storage.__wrapped__(
|
||||
mock_task,
|
||||
original_file="/tmp/original.pdf",
|
||||
processed_file="/workdir/processed/test.pdf",
|
||||
metadata={"filename": "test.pdf"},
|
||||
@@ -171,8 +165,8 @@ class TestFinalizeDocumentStorage:
|
||||
# Should still send notification
|
||||
mock_notify.assert_called_once()
|
||||
notify_args = mock_notify.call_args[1]
|
||||
# Should have fallback destination text
|
||||
assert len(notify_args["destinations"]) > 0
|
||||
# No services configured means empty destinations list
|
||||
assert notify_args["destinations"] == []
|
||||
|
||||
@patch("app.tasks.finalize_document_storage.notify_file_processed")
|
||||
@patch("app.tasks.finalize_document_storage.send_to_all_destinations")
|
||||
@@ -198,11 +192,9 @@ class TestFinalizeDocumentStorage:
|
||||
with patch("app.tasks.finalize_document_storage.os.path.exists", return_value=True):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.getsize", return_value=2048):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.basename", return_value="file.pdf"):
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
finalize_document_storage.request.id = "test-task-id"
|
||||
|
||||
result = finalize_document_storage.__wrapped__(
|
||||
mock_task,
|
||||
original_file="/tmp/original.pdf",
|
||||
processed_file="/workdir/processed/file.pdf",
|
||||
metadata={"filename": "file.pdf"},
|
||||
@@ -243,11 +235,9 @@ class TestFinalizeDocumentStorage:
|
||||
with patch("app.tasks.finalize_document_storage.os.path.exists", return_value=True):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.getsize", return_value=4096):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.basename", return_value="doc.pdf"):
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
finalize_document_storage.request.id = "test-task-id"
|
||||
|
||||
result = finalize_document_storage.__wrapped__(
|
||||
mock_task,
|
||||
original_file="/tmp/original.pdf",
|
||||
processed_file="/workdir/processed/doc.pdf",
|
||||
metadata={"filename": "doc.pdf"},
|
||||
@@ -283,11 +273,9 @@ class TestFinalizeDocumentStorage:
|
||||
# File doesn't exist
|
||||
with patch("app.tasks.finalize_document_storage.os.path.exists", return_value=False):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.basename", return_value="missing.pdf"):
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
finalize_document_storage.request.id = "test-task-id"
|
||||
|
||||
result = finalize_document_storage.__wrapped__(
|
||||
mock_task,
|
||||
original_file="/tmp/original.pdf",
|
||||
processed_file="/workdir/processed/missing.pdf",
|
||||
metadata={"filename": "missing.pdf"},
|
||||
@@ -330,11 +318,9 @@ class TestFinalizeDocumentStorage:
|
||||
with patch("app.tasks.finalize_document_storage.os.path.exists", return_value=True):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.getsize", return_value=8192):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.basename", return_value="test.pdf"):
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
finalize_document_storage.request.id = "test-task-id"
|
||||
|
||||
result = finalize_document_storage.__wrapped__(
|
||||
mock_task,
|
||||
original_file="/tmp/original.pdf",
|
||||
processed_file="/workdir/processed/test.pdf",
|
||||
metadata={"filename": "test.pdf"},
|
||||
@@ -372,11 +358,9 @@ class TestFinalizeDocumentStorage:
|
||||
with patch("app.tasks.finalize_document_storage.os.path.exists", return_value=True):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.getsize", return_value=1024):
|
||||
with patch("app.tasks.finalize_document_storage.os.path.basename", return_value="file.pdf"):
|
||||
mock_task = MagicMock()
|
||||
mock_task.request.id = "test-task-id"
|
||||
finalize_document_storage.request.id = "test-task-id"
|
||||
|
||||
result = finalize_document_storage.__wrapped__(
|
||||
mock_task,
|
||||
original_file="/tmp/original.pdf",
|
||||
processed_file="/workdir/processed/file.pdf",
|
||||
metadata={"filename": "file.pdf"},
|
||||
|
||||
@@ -50,7 +50,7 @@ class TestOAuthLoginFlow:
|
||||
try:
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
client = TestClient(app)
|
||||
client = TestClient(app, base_url="http://localhost")
|
||||
|
||||
response = client.get("/oauth-login", follow_redirects=False)
|
||||
# Should either redirect to error page or show login page
|
||||
@@ -64,6 +64,7 @@ class TestOAuthLoginFlow:
|
||||
class TestOAuthCallback:
|
||||
"""Test OAuth callback handling."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("app.auth.oauth.authentik.authorize_access_token")
|
||||
async def test_oauth_callback_with_valid_token(
|
||||
self, mock_authorize, oauth_enabled_app: TestClient, test_user_info: dict
|
||||
@@ -86,6 +87,7 @@ class TestOAuthCallback:
|
||||
# Should redirect after successful login
|
||||
assert response.status_code == 302
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("app.auth.oauth.authentik.authorize_access_token")
|
||||
async def test_oauth_callback_stores_user_in_session(
|
||||
self, mock_authorize, oauth_enabled_app: TestClient, test_user_info: dict
|
||||
@@ -108,6 +110,7 @@ class TestOAuthCallback:
|
||||
# Should set session cookie
|
||||
assert "set-cookie" in response.headers or response.status_code == 302
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("app.auth.oauth.authentik.authorize_access_token")
|
||||
async def test_oauth_callback_with_admin_user(
|
||||
self, mock_authorize, oauth_enabled_app: TestClient
|
||||
@@ -131,11 +134,12 @@ class TestOAuthCallback:
|
||||
# Should successfully authenticate
|
||||
assert response.status_code == 302
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("app.auth.oauth.authentik.authorize_access_token")
|
||||
async def test_oauth_callback_rejects_non_admin(
|
||||
self, mock_authorize, oauth_enabled_app: TestClient
|
||||
):
|
||||
"""Test that OAuth callback rejects users without admin group."""
|
||||
"""Test that OAuth callback authenticates non-admin users with is_admin=False."""
|
||||
mock_authorize.return_value = {
|
||||
"access_token": "mock-access-token",
|
||||
"userinfo": {
|
||||
@@ -151,15 +155,15 @@ class TestOAuthCallback:
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
# Should redirect to error page
|
||||
# Non-admin users are still authenticated but with is_admin=False
|
||||
assert response.status_code == 302
|
||||
assert "error" in response.headers.get("location", "").lower()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestOAuthSessionManagement:
|
||||
"""Test session management with OAuth authentication."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("app.auth.oauth.authentik.authorize_access_token")
|
||||
async def test_authenticated_user_can_access_protected_routes(
|
||||
self, mock_authorize, oauth_enabled_app: TestClient, test_user_info: dict
|
||||
@@ -192,6 +196,7 @@ class TestOAuthSessionManagement:
|
||||
if response.status_code == 302:
|
||||
assert "/login" in response.headers.get("location", "")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("app.auth.oauth.authentik.authorize_access_token")
|
||||
async def test_logout_clears_session(
|
||||
self, mock_authorize, oauth_enabled_app: TestClient, test_user_info: dict
|
||||
@@ -226,6 +231,7 @@ class TestOAuthErrorHandling:
|
||||
# Should handle error gracefully
|
||||
assert response.status_code in [302, 400]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("app.auth.oauth.authentik.authorize_access_token")
|
||||
async def test_oauth_callback_with_invalid_token(
|
||||
self, mock_authorize, oauth_enabled_app: TestClient
|
||||
@@ -244,6 +250,7 @@ class TestOAuthErrorHandling:
|
||||
location = response.headers.get("location", "")
|
||||
assert "error" in location.lower() or "login" in location.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("app.auth.oauth.authentik.authorize_access_token")
|
||||
async def test_oauth_callback_without_userinfo(
|
||||
self, mock_authorize, oauth_enabled_app: TestClient
|
||||
|
||||
@@ -136,7 +136,7 @@ class TestRateLimitDecorators:
|
||||
|
||||
# Mock limiter to return a simple passthrough decorator
|
||||
mock_limiter = MagicMock()
|
||||
mock_limiter.exempt.return_value = lambda f: f
|
||||
mock_limiter.exempt.side_effect = lambda f: f
|
||||
mock_get_limiter.return_value = mock_limiter
|
||||
|
||||
# Decorate function
|
||||
|
||||
+16
-19
@@ -44,8 +44,8 @@ class TestStepTimeout:
|
||||
|
||||
# Mock database session with proper query chain
|
||||
mock_db = MagicMock()
|
||||
# Set up the query chain to return empty list
|
||||
mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.all.return_value = []
|
||||
# Set up the query chain to return empty list (single .filter() call with multiple conditions)
|
||||
mock_db.query.return_value.filter.return_value.all.return_value = []
|
||||
|
||||
# Run function
|
||||
count = mark_stalled_steps_as_failed(mock_db)
|
||||
@@ -74,8 +74,8 @@ class TestStepTimeout:
|
||||
|
||||
# Mock database session
|
||||
mock_db = MagicMock()
|
||||
# Set up the query chain to return stalled steps
|
||||
mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.all.return_value = [step1, step2]
|
||||
# Set up the query chain to return stalled steps (single .filter() call with multiple conditions)
|
||||
mock_db.query.return_value.filter.return_value.all.return_value = [step1, step2]
|
||||
|
||||
# Run function
|
||||
count = mark_stalled_steps_as_failed(mock_db)
|
||||
@@ -105,8 +105,8 @@ class TestStepTimeout:
|
||||
|
||||
# Mock database session
|
||||
mock_db = MagicMock()
|
||||
# Set up the query chain to return stalled step
|
||||
mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.all.return_value = [step]
|
||||
# Set up the query chain to return stalled step (single .filter() call with multiple conditions)
|
||||
mock_db.query.return_value.filter.return_value.all.return_value = [step]
|
||||
|
||||
# Run function with 150 second timeout
|
||||
count = mark_stalled_steps_as_failed(mock_db, timeout_seconds=150)
|
||||
@@ -131,9 +131,8 @@ class TestStepTimeout:
|
||||
|
||||
# Mock database session with file filter
|
||||
mock_db = MagicMock()
|
||||
# Set up the query chain with file filter
|
||||
mock_query_chain = mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value
|
||||
mock_query_chain.filter.return_value.all.return_value = [step]
|
||||
# Set up the query chain with file filter (first .filter() for conditions, second for file_id)
|
||||
mock_db.query.return_value.filter.return_value.filter.return_value.all.return_value = [step]
|
||||
|
||||
# Run function for specific file
|
||||
count = mark_stalled_steps_as_failed(mock_db, file_id=42)
|
||||
@@ -158,8 +157,8 @@ class TestStepTimeout:
|
||||
|
||||
# Mock database session
|
||||
mock_db = MagicMock()
|
||||
# Set up the query chain to return stalled step
|
||||
mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.all.return_value = [step]
|
||||
# Set up the query chain to return stalled step (single .filter() call with multiple conditions)
|
||||
mock_db.query.return_value.filter.return_value.all.return_value = [step]
|
||||
|
||||
# Run function
|
||||
count = mark_stalled_steps_as_failed(mock_db, timeout_seconds=600)
|
||||
@@ -186,8 +185,8 @@ class TestStepTimeout:
|
||||
|
||||
# Mock database session
|
||||
mock_db = MagicMock()
|
||||
# Set up the query chain to return stalled step
|
||||
mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.all.return_value = [step]
|
||||
# Set up the query chain to return stalled step (single .filter() call with multiple conditions)
|
||||
mock_db.query.return_value.filter.return_value.all.return_value = [step]
|
||||
|
||||
# Run function
|
||||
count = mark_stalled_steps_as_failed(mock_db)
|
||||
@@ -212,9 +211,8 @@ class TestStepTimeout:
|
||||
|
||||
# Mock database session
|
||||
mock_db = MagicMock()
|
||||
# Set up the query chain with file filter
|
||||
mock_query_chain = mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value
|
||||
mock_query_chain.filter.return_value.all.return_value = [step]
|
||||
# Set up the query chain with file filter (first .filter() for conditions, second for file_id)
|
||||
mock_db.query.return_value.filter.return_value.filter.return_value.all.return_value = [step]
|
||||
|
||||
# Run function
|
||||
result = check_and_recover_stalled_file(mock_db, 42)
|
||||
@@ -229,9 +227,8 @@ class TestStepTimeout:
|
||||
|
||||
# Mock database session with no stalled steps
|
||||
mock_db = MagicMock()
|
||||
# Set up the query chain with file filter
|
||||
mock_query_chain = mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value
|
||||
mock_query_chain.filter.return_value.all.return_value = []
|
||||
# Set up the query chain with file filter (first .filter() for conditions, second for file_id)
|
||||
mock_db.query.return_value.filter.return_value.filter.return_value.all.return_value = []
|
||||
|
||||
# Run function
|
||||
result = check_and_recover_stalled_file(mock_db, 42)
|
||||
|
||||
Reference in New Issue
Block a user