c7d3ec57c3
Commitd2217531(google-labs-jules SSRF fix) catastrophically deleted 11,500+ lines across 100+ files while fixing an unrelated IMAP issue. Restored from d2217531^ (pre-bad-commit state): Deleted files (fully restored): - app/api/{automation,classification_rules,comments,sharing}.py - app/middleware/upload_rate_limit.py - app/tasks/{automation_tasks,classify_document}.py - app/utils/{automation_hooks,classification_rules}.py - docs/AppleAppStoreCompliance.md - frontend/input.css, package.json, package-lock.json, tailwind.config.js - frontend/static/js/{annotations,claim,comments,sharing}.js - frontend/templates/{admin_connections,file_annotations,file_summary}.html - tests/{test_api_files_comprehensive,test_auth_extended,test_sharing, test_comments,test_connections,test_imap_profiles,test_api_sessions, test_automation,test_classification_rules,test_api_advanced_filters, test_api_classification_rules,test_upload_rate_limit,test_api_dropbox, test_classify_document,test_comments_ui,test_upload_to_icloud, test_api_onedrive_comprehensive,test_frontend_build,test_sentry, test_diagnostic,test_database,test_views_dropbox,test_local_auth}.py Truncated files (content restored): - app/{auth,config,main,models,celery_worker,database}.py - app/api/{__init__,api_tokens,diagnostic,dropbox,files,google_drive, integrations,local_auth,mobile,onedrive,pipelines,qr_auth, settings,url_upload}.py - app/middleware/upload_rate_limit.py - app/tasks/upload_to_nextcloud.py - app/utils/{allowed_types,settings_service,settings_sync,user_scope,webhook}.py - app/views/{base,dropbox,files,google_drive,onedrive,settings}.py - docs/{API,AuthenticationSetup,ConfigurationGuide,DatabaseConfiguration, DeploymentGuide,DropboxSetup,GoogleDriveSetup,KubernetesDeployment, MobileApp,OneDriveSetup,ProductionReadiness,SentrySetup, SocialLoginSetup,UserGuide}.md - frontend/static/{js/upload.js,styles.css} - frontend/templates/{api_tokens,base,devices,dropbox,dropbox_callback, file_view,files,google_drive,onedrive,onedrive_callback, signup}.html - frontend/translations/en.json - migrations/env.py - tests/{conftest,test_api_integrations,test_api_mobile,test_api_settings, test_api_tokens,test_audit_logs,test_duplicates,test_imap_tasks, test_setup_wizard,test_views_files_comprehensive}.py Security fixes kept from post-d2217531 commits: - app/utils/network.py: DNS SSRF fail-secure fix (06b0fced) - app/utils/file_operations.py: path traversal fix (1018ea17) - tests/test_imap_tasks.py: re-applied 4 is_private_ip mock patches Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/51133dd8-9bec-41ab-aa10-3de753634187
617 lines
25 KiB
Python
617 lines
25 KiB
Python
"""Tests for app/api/imap_profiles.py and app/utils/allowed_types category helpers."""
|
||
|
||
import pytest
|
||
from fastapi.testclient import TestClient
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.orm import sessionmaker
|
||
from sqlalchemy.pool import StaticPool
|
||
|
||
from app.database import Base, get_db
|
||
from app.models import ImapIngestionProfile
|
||
from app.utils.allowed_types import (
|
||
ALL_CATEGORIES,
|
||
DEFAULT_CATEGORIES,
|
||
FILE_TYPE_CATEGORIES,
|
||
get_allowed_types_for_categories,
|
||
)
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Integration test constants
|
||
# ---------------------------------------------------------------------------
|
||
|
||
_OWNER = "profile_user@example.com"
|
||
_OTHER = "other_user@example.com"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Shared integration fixtures
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.fixture()
|
||
def profile_engine():
|
||
"""In-memory SQLite engine for IMAP profile tests."""
|
||
engine = create_engine(
|
||
"sqlite:///:memory:",
|
||
connect_args={"check_same_thread": False},
|
||
poolclass=StaticPool,
|
||
)
|
||
Base.metadata.create_all(bind=engine)
|
||
yield engine
|
||
Base.metadata.drop_all(bind=engine)
|
||
|
||
|
||
@pytest.fixture()
|
||
def profile_session(profile_engine):
|
||
"""DB session scoped to one test."""
|
||
Session = sessionmaker(bind=profile_engine)
|
||
session = Session()
|
||
yield session
|
||
session.close()
|
||
|
||
|
||
@pytest.fixture()
|
||
def profile_client(profile_engine):
|
||
"""TestClient authenticated as _OWNER with DB overridden."""
|
||
from app.api.imap_profiles import _get_owner_id
|
||
from app.main import app
|
||
|
||
def override_db():
|
||
Session = sessionmaker(bind=profile_engine)
|
||
session = Session()
|
||
try:
|
||
yield session
|
||
finally:
|
||
session.close()
|
||
|
||
def override_owner():
|
||
return _OWNER
|
||
|
||
app.dependency_overrides[get_db] = override_db
|
||
app.dependency_overrides[_get_owner_id] = override_owner
|
||
with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as c:
|
||
yield c
|
||
app.dependency_overrides.clear()
|
||
|
||
|
||
@pytest.fixture()
|
||
def anon_client(profile_engine):
|
||
"""TestClient without authentication (DB still overridden)."""
|
||
from app.main import app
|
||
|
||
def override_db():
|
||
Session = sessionmaker(bind=profile_engine)
|
||
session = Session()
|
||
try:
|
||
yield session
|
||
finally:
|
||
session.close()
|
||
|
||
app.dependency_overrides[get_db] = override_db
|
||
with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as c:
|
||
yield c
|
||
app.dependency_overrides.clear()
|
||
|
||
|
||
def _make_profile(
|
||
session,
|
||
owner: str | None = _OWNER,
|
||
name: str = "Test Profile",
|
||
categories_json: str = '["pdf","office"]',
|
||
is_builtin: bool = False,
|
||
) -> ImapIngestionProfile:
|
||
"""Create an ImapIngestionProfile row in the database."""
|
||
prof = ImapIngestionProfile(
|
||
name=name,
|
||
description="A test profile",
|
||
owner_id=owner,
|
||
allowed_categories=categories_json,
|
||
is_builtin=is_builtin,
|
||
)
|
||
session.add(prof)
|
||
session.commit()
|
||
session.refresh(prof)
|
||
return prof
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestFileTypeCategories:
|
||
"""Tests for FILE_TYPE_CATEGORIES and get_allowed_types_for_categories."""
|
||
|
||
def test_all_category_keys_present(self):
|
||
"""Test that the six expected categories exist."""
|
||
assert set(FILE_TYPE_CATEGORIES.keys()) == {"pdf", "office", "opendocument", "text", "web", "images"}
|
||
|
||
def test_each_category_has_required_fields(self):
|
||
"""Test that every category entry has label, description, mime_types, extensions."""
|
||
for key, info in FILE_TYPE_CATEGORIES.items():
|
||
assert "label" in info, f"Category '{key}' missing 'label'"
|
||
assert "description" in info, f"Category '{key}' missing 'description'"
|
||
assert "mime_types" in info, f"Category '{key}' missing 'mime_types'"
|
||
assert "extensions" in info, f"Category '{key}' missing 'extensions'"
|
||
|
||
def test_pdf_category_contains_pdf_mime(self):
|
||
"""Test that the pdf category includes application/pdf."""
|
||
assert "application/pdf" in FILE_TYPE_CATEGORIES["pdf"]["mime_types"]
|
||
assert ".pdf" in FILE_TYPE_CATEGORIES["pdf"]["extensions"]
|
||
|
||
def test_images_category_contains_jpeg(self):
|
||
"""Test that the images category includes image/jpeg."""
|
||
assert "image/jpeg" in FILE_TYPE_CATEGORIES["images"]["mime_types"]
|
||
assert ".jpg" in FILE_TYPE_CATEGORIES["images"]["extensions"]
|
||
assert ".png" in FILE_TYPE_CATEGORIES["images"]["extensions"]
|
||
|
||
def test_get_allowed_types_for_default_categories(self):
|
||
"""Test that DEFAULT_CATEGORIES excludes image MIME types."""
|
||
mime_types, extensions = get_allowed_types_for_categories(DEFAULT_CATEGORIES)
|
||
assert "application/pdf" in mime_types
|
||
assert "application/msword" in mime_types
|
||
# images should NOT be in the default
|
||
assert "image/jpeg" not in mime_types
|
||
assert ".jpg" not in extensions
|
||
|
||
def test_get_allowed_types_for_all_categories(self):
|
||
"""Test that ALL_CATEGORIES includes image MIME types."""
|
||
mime_types, extensions = get_allowed_types_for_categories(ALL_CATEGORIES)
|
||
assert "image/jpeg" in mime_types
|
||
assert ".jpg" in extensions
|
||
assert "application/pdf" in mime_types
|
||
|
||
def test_get_allowed_types_returns_frozensets(self):
|
||
"""Test that returned sets are frozensets."""
|
||
mime_types, extensions = get_allowed_types_for_categories(["pdf"])
|
||
assert isinstance(mime_types, frozenset)
|
||
assert isinstance(extensions, frozenset)
|
||
|
||
def test_get_allowed_types_unknown_category_ignored(self):
|
||
"""Test that unknown category keys are silently ignored."""
|
||
mime_types, extensions = get_allowed_types_for_categories(["pdf", "nonexistent_category"])
|
||
assert "application/pdf" in mime_types # 'pdf' still works
|
||
# No crash for unknown key
|
||
|
||
def test_get_allowed_types_empty_list(self):
|
||
"""Test empty category list returns empty sets."""
|
||
mime_types, extensions = get_allowed_types_for_categories([])
|
||
assert mime_types == frozenset()
|
||
assert extensions == frozenset()
|
||
|
||
def test_default_categories_excludes_images(self):
|
||
"""Test that DEFAULT_CATEGORIES does not include 'images'."""
|
||
assert "images" not in DEFAULT_CATEGORIES
|
||
|
||
def test_all_categories_includes_images(self):
|
||
"""Test that ALL_CATEGORIES includes 'images'."""
|
||
assert "images" in ALL_CATEGORIES
|
||
|
||
def test_all_categories_is_superset_of_default(self):
|
||
"""Test that ALL_CATEGORIES contains all DEFAULT_CATEGORIES."""
|
||
for cat in DEFAULT_CATEGORIES:
|
||
assert cat in ALL_CATEGORIES
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestImapProfilesApiLogic:
|
||
"""Tests for ingestion profile validation helpers."""
|
||
|
||
def test_validate_categories_accepts_valid_keys(self):
|
||
"""Test that valid category keys pass validation."""
|
||
from app.api.imap_profiles import _validate_categories
|
||
|
||
result = _validate_categories(["pdf", "office", "images"])
|
||
assert set(result) == {"pdf", "office", "images"}
|
||
|
||
def test_validate_categories_rejects_unknown_key(self):
|
||
"""Test that unknown category keys raise 422."""
|
||
from fastapi import HTTPException
|
||
|
||
from app.api.imap_profiles import _validate_categories
|
||
|
||
with pytest.raises(HTTPException) as exc_info:
|
||
_validate_categories(["pdf", "nonexistent"])
|
||
assert exc_info.value.status_code == 422
|
||
assert "nonexistent" in str(exc_info.value.detail)
|
||
|
||
def test_validate_categories_deduplicates(self):
|
||
"""Test that duplicate category keys are de-duplicated while preserving order."""
|
||
from app.api.imap_profiles import _validate_categories
|
||
|
||
result = _validate_categories(["pdf", "pdf", "office", "pdf"])
|
||
assert result == ["pdf", "office"]
|
||
|
||
def test_to_response_serializes_profile(self, tmp_path):
|
||
"""Test _to_response produces expected dict shape."""
|
||
from unittest.mock import MagicMock
|
||
|
||
from app.api.imap_profiles import _to_response
|
||
|
||
profile = MagicMock()
|
||
profile.id = 42
|
||
profile.name = "My Profile"
|
||
profile.description = "Test description"
|
||
profile.owner_id = "user@example.com"
|
||
profile.allowed_categories = '["pdf","office"]'
|
||
profile.is_builtin = False
|
||
profile.created_at = None
|
||
profile.updated_at = None
|
||
|
||
result = _to_response(profile)
|
||
assert result["id"] == 42
|
||
assert result["name"] == "My Profile"
|
||
assert result["allowed_categories"] == ["pdf", "office"]
|
||
assert len(result["categories_detail"]) == 2
|
||
assert result["categories_detail"][0]["key"] == "pdf"
|
||
assert result["is_builtin"] is False
|
||
|
||
def test_to_response_handles_invalid_categories_json(self):
|
||
"""Test _to_response gracefully handles invalid JSON in allowed_categories."""
|
||
from unittest.mock import MagicMock
|
||
|
||
from app.api.imap_profiles import _to_response
|
||
|
||
profile = MagicMock()
|
||
profile.id = 1
|
||
profile.name = "Broken"
|
||
profile.description = None
|
||
profile.owner_id = None
|
||
profile.allowed_categories = "this is not valid json {"
|
||
profile.is_builtin = True
|
||
profile.created_at = None
|
||
profile.updated_at = None
|
||
|
||
result = _to_response(profile)
|
||
assert result["allowed_categories"] == []
|
||
|
||
def test_get_owner_id_returns_owner_when_authenticated(self):
|
||
"""Test that _get_owner_id returns the owner_id when get_current_owner_id succeeds."""
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
from app.api.imap_profiles import _get_owner_id
|
||
|
||
request = MagicMock()
|
||
with patch("app.api.imap_profiles.get_current_owner_id", return_value="user@example.com"):
|
||
result = _get_owner_id(request)
|
||
assert result == "user@example.com"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Integration tests – list categories endpoint
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
class TestListCategories:
|
||
"""Tests for GET /api/imap-profiles/categories."""
|
||
|
||
def test_list_categories_returns_all(self, profile_client):
|
||
"""Authenticated request returns all available file-type categories."""
|
||
resp = profile_client.get("/api/imap-profiles/categories")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert isinstance(data, list)
|
||
keys = {item["key"] for item in data}
|
||
assert keys == set(FILE_TYPE_CATEGORIES.keys())
|
||
for item in data:
|
||
assert "key" in item
|
||
assert "label" in item
|
||
assert "description" in item
|
||
|
||
def test_list_categories_unauthenticated(self, anon_client):
|
||
"""Unauthenticated request returns 401."""
|
||
resp = anon_client.get("/api/imap-profiles/categories")
|
||
assert resp.status_code == 401
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Integration tests – list profiles endpoint
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
class TestListProfiles:
|
||
"""Tests for GET /api/imap-profiles/."""
|
||
|
||
def test_list_empty(self, profile_client, profile_session):
|
||
"""Listing profiles when none exist returns an empty list."""
|
||
resp = profile_client.get("/api/imap-profiles/")
|
||
assert resp.status_code == 200
|
||
assert resp.json() == []
|
||
|
||
def test_list_includes_own_profiles(self, profile_client, profile_session):
|
||
"""Returns profiles owned by the current user."""
|
||
_make_profile(profile_session, owner=_OWNER, name="My Profile")
|
||
resp = profile_client.get("/api/imap-profiles/")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert any(p["name"] == "My Profile" for p in data)
|
||
|
||
def test_list_includes_global_profiles(self, profile_client, profile_session):
|
||
"""Returns system-global profiles (owner_id=None)."""
|
||
_make_profile(profile_session, owner=None, name="Global Profile", is_builtin=True)
|
||
resp = profile_client.get("/api/imap-profiles/")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert any(p["name"] == "Global Profile" for p in data)
|
||
|
||
def test_list_excludes_other_users_profiles(self, profile_client, profile_session):
|
||
"""Profiles owned by other users are not returned."""
|
||
_make_profile(profile_session, owner=_OTHER, name="Other Profile")
|
||
resp = profile_client.get("/api/imap-profiles/")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert not any(p["name"] == "Other Profile" for p in data)
|
||
|
||
def test_list_unauthenticated(self, anon_client):
|
||
"""Unauthenticated request returns 401."""
|
||
resp = anon_client.get("/api/imap-profiles/")
|
||
assert resp.status_code == 401
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Integration tests – create profile endpoint
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
class TestCreateProfile:
|
||
"""Tests for POST /api/imap-profiles/."""
|
||
|
||
def test_create_success(self, profile_client, profile_session):
|
||
"""Creating a valid profile returns 201 with the new profile data."""
|
||
payload = {"name": "New Profile", "description": "desc", "allowed_categories": ["pdf", "office"]}
|
||
resp = profile_client.post("/api/imap-profiles/", json=payload)
|
||
assert resp.status_code == 201
|
||
data = resp.json()
|
||
assert data["name"] == "New Profile"
|
||
assert data["description"] == "desc"
|
||
assert data["allowed_categories"] == ["pdf", "office"]
|
||
assert data["is_builtin"] is False
|
||
assert data["owner_id"] == _OWNER
|
||
assert "id" in data
|
||
|
||
def test_create_deduplicates_categories(self, profile_client):
|
||
"""Duplicate categories in the request are de-duplicated."""
|
||
payload = {"name": "Dedup Profile", "allowed_categories": ["pdf", "pdf", "office"]}
|
||
resp = profile_client.post("/api/imap-profiles/", json=payload)
|
||
assert resp.status_code == 201
|
||
assert resp.json()["allowed_categories"] == ["pdf", "office"]
|
||
|
||
def test_create_invalid_category_returns_422(self, profile_client):
|
||
"""Unknown category keys cause a 422 response."""
|
||
payload = {"name": "Bad Profile", "allowed_categories": ["pdf", "nonexistent"]}
|
||
resp = profile_client.post("/api/imap-profiles/", json=payload)
|
||
assert resp.status_code == 422
|
||
|
||
def test_create_missing_name_returns_422(self, profile_client):
|
||
"""Missing required 'name' field causes a 422 response."""
|
||
payload = {"allowed_categories": ["pdf"]}
|
||
resp = profile_client.post("/api/imap-profiles/", json=payload)
|
||
assert resp.status_code == 422
|
||
|
||
def test_create_empty_categories_returns_422(self, profile_client):
|
||
"""An empty allowed_categories list causes a 422 response."""
|
||
payload = {"name": "Empty Cats", "allowed_categories": []}
|
||
resp = profile_client.post("/api/imap-profiles/", json=payload)
|
||
assert resp.status_code == 422
|
||
|
||
def test_create_unauthenticated(self, anon_client):
|
||
"""Unauthenticated request returns 401."""
|
||
payload = {"name": "X", "allowed_categories": ["pdf"]}
|
||
resp = anon_client.post("/api/imap-profiles/", json=payload)
|
||
assert resp.status_code == 401
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Integration tests – get single profile endpoint
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
class TestGetProfile:
|
||
"""Tests for GET /api/imap-profiles/{id}."""
|
||
|
||
def test_get_own_profile(self, profile_client, profile_session):
|
||
"""Owner can retrieve their own profile."""
|
||
prof = _make_profile(profile_session, owner=_OWNER)
|
||
resp = profile_client.get(f"/api/imap-profiles/{prof.id}")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["id"] == prof.id
|
||
assert data["name"] == prof.name
|
||
|
||
def test_get_global_profile(self, profile_client, profile_session):
|
||
"""Any authenticated user can retrieve a global (owner_id=None) profile."""
|
||
prof = _make_profile(profile_session, owner=None, name="Builtin", is_builtin=True)
|
||
resp = profile_client.get(f"/api/imap-profiles/{prof.id}")
|
||
assert resp.status_code == 200
|
||
assert resp.json()["name"] == "Builtin"
|
||
|
||
def test_get_not_found(self, profile_client):
|
||
"""Requesting a non-existent profile returns 404."""
|
||
resp = profile_client.get("/api/imap-profiles/99999")
|
||
assert resp.status_code == 404
|
||
|
||
def test_get_other_user_profile_returns_404(self, profile_client, profile_session):
|
||
"""Accessing another user's private profile returns 404."""
|
||
prof = _make_profile(profile_session, owner=_OTHER, name="Private")
|
||
resp = profile_client.get(f"/api/imap-profiles/{prof.id}")
|
||
assert resp.status_code == 404
|
||
|
||
def test_get_unauthenticated(self, anon_client, profile_session):
|
||
"""Unauthenticated request returns 401."""
|
||
prof = _make_profile(profile_session, owner=None, is_builtin=True)
|
||
resp = anon_client.get(f"/api/imap-profiles/{prof.id}")
|
||
assert resp.status_code == 401
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Integration tests – update profile endpoint
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
class TestUpdateProfile:
|
||
"""Tests for PUT /api/imap-profiles/{id}."""
|
||
|
||
def test_update_name(self, profile_client, profile_session):
|
||
"""Updating the name of an owned profile returns the updated profile."""
|
||
prof = _make_profile(profile_session, owner=_OWNER, name="Old Name")
|
||
resp = profile_client.put(f"/api/imap-profiles/{prof.id}", json={"name": "New Name"})
|
||
assert resp.status_code == 200
|
||
assert resp.json()["name"] == "New Name"
|
||
|
||
def test_update_categories(self, profile_client, profile_session):
|
||
"""Updating allowed_categories replaces the previous value."""
|
||
prof = _make_profile(profile_session, owner=_OWNER, categories_json='["pdf"]')
|
||
resp = profile_client.put(f"/api/imap-profiles/{prof.id}", json={"allowed_categories": ["office", "text"]})
|
||
assert resp.status_code == 200
|
||
assert resp.json()["allowed_categories"] == ["office", "text"]
|
||
|
||
def test_update_description(self, profile_client, profile_session):
|
||
"""Setting description via model_fields_set path updates it."""
|
||
prof = _make_profile(profile_session, owner=_OWNER)
|
||
resp = profile_client.put(f"/api/imap-profiles/{prof.id}", json={"description": "Updated desc"})
|
||
assert resp.status_code == 200
|
||
assert resp.json()["description"] == "Updated desc"
|
||
|
||
def test_update_builtin_returns_403(self, profile_client, profile_session):
|
||
"""Attempting to update a built-in profile returns 403."""
|
||
prof = _make_profile(profile_session, owner=None, is_builtin=True)
|
||
resp = profile_client.put(f"/api/imap-profiles/{prof.id}", json={"name": "Renamed"})
|
||
assert resp.status_code == 403
|
||
|
||
def test_update_not_found(self, profile_client):
|
||
"""Updating a non-existent profile returns 404."""
|
||
resp = profile_client.put("/api/imap-profiles/99999", json={"name": "X"})
|
||
assert resp.status_code == 404
|
||
|
||
def test_update_other_user_profile_returns_404(self, profile_client, profile_session):
|
||
"""Updating another user's profile returns 404."""
|
||
prof = _make_profile(profile_session, owner=_OTHER)
|
||
resp = profile_client.put(f"/api/imap-profiles/{prof.id}", json={"name": "X"})
|
||
assert resp.status_code == 404
|
||
|
||
def test_update_invalid_category_returns_422(self, profile_client, profile_session):
|
||
"""Updating with an invalid category key returns 422."""
|
||
prof = _make_profile(profile_session, owner=_OWNER)
|
||
resp = profile_client.put(f"/api/imap-profiles/{prof.id}", json={"allowed_categories": ["badcat"]})
|
||
assert resp.status_code == 422
|
||
|
||
def test_update_unauthenticated(self, anon_client, profile_session):
|
||
"""Unauthenticated request returns 401."""
|
||
prof = _make_profile(profile_session, owner=_OWNER)
|
||
resp = anon_client.put(f"/api/imap-profiles/{prof.id}", json={"name": "X"})
|
||
assert resp.status_code == 401
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Integration tests – delete profile endpoint
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
class TestDeleteProfile:
|
||
"""Tests for DELETE /api/imap-profiles/{id}."""
|
||
|
||
def test_delete_success(self, profile_client, profile_session):
|
||
"""Deleting an owned profile returns 204 and removes the row."""
|
||
prof = _make_profile(profile_session, owner=_OWNER)
|
||
resp = profile_client.delete(f"/api/imap-profiles/{prof.id}")
|
||
assert resp.status_code == 204
|
||
# Verify it is gone
|
||
get_resp = profile_client.get(f"/api/imap-profiles/{prof.id}")
|
||
assert get_resp.status_code == 404
|
||
|
||
def test_delete_builtin_returns_403(self, profile_client, profile_session):
|
||
"""Attempting to delete a built-in profile returns 403."""
|
||
prof = _make_profile(profile_session, owner=None, is_builtin=True)
|
||
resp = profile_client.delete(f"/api/imap-profiles/{prof.id}")
|
||
assert resp.status_code == 403
|
||
|
||
def test_delete_not_found(self, profile_client):
|
||
"""Deleting a non-existent profile returns 404."""
|
||
resp = profile_client.delete("/api/imap-profiles/99999")
|
||
assert resp.status_code == 404
|
||
|
||
def test_delete_other_user_profile_returns_404(self, profile_client, profile_session):
|
||
"""Deleting another user's private profile returns 404."""
|
||
prof = _make_profile(profile_session, owner=_OTHER)
|
||
resp = profile_client.delete(f"/api/imap-profiles/{prof.id}")
|
||
assert resp.status_code == 404
|
||
|
||
def test_delete_unauthenticated(self, anon_client, profile_session):
|
||
"""Unauthenticated request returns 401."""
|
||
prof = _make_profile(profile_session, owner=_OWNER)
|
||
resp = anon_client.delete(f"/api/imap-profiles/{prof.id}")
|
||
assert resp.status_code == 401
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Integration tests – DB error rollback paths
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
class TestDbErrorRollback:
|
||
"""Tests for exception-handling / rollback paths in create, update, and delete."""
|
||
|
||
def _make_failing_client(self, profile_engine, profile_session, *, fail_on: str = "commit"):
|
||
"""Return a TestClient whose DB session raises RuntimeError on commit/delete."""
|
||
from app.api.imap_profiles import _get_owner_id
|
||
from app.main import app
|
||
|
||
Session = sessionmaker(bind=profile_engine)
|
||
|
||
def override_db():
|
||
session = Session()
|
||
|
||
def raise_error(*args, **kwargs):
|
||
raise RuntimeError("Simulated DB failure")
|
||
|
||
setattr(session, fail_on, raise_error)
|
||
try:
|
||
yield session
|
||
finally:
|
||
session.close()
|
||
|
||
def override_owner():
|
||
return _OWNER
|
||
|
||
app.dependency_overrides[get_db] = override_db
|
||
app.dependency_overrides[_get_owner_id] = override_owner
|
||
return TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
|
||
|
||
def test_create_db_error_returns_500(self, profile_engine, profile_session):
|
||
"""A DB error during create triggers rollback and returns 500."""
|
||
client = self._make_failing_client(profile_engine, profile_session)
|
||
try:
|
||
resp = client.post("/api/imap-profiles/", json={"name": "X", "allowed_categories": ["pdf"]})
|
||
finally:
|
||
from app.main import app
|
||
|
||
app.dependency_overrides.clear()
|
||
assert resp.status_code == 500
|
||
|
||
def test_update_db_error_returns_500(self, profile_engine, profile_session):
|
||
"""A DB error during update triggers rollback and returns 500."""
|
||
prof = _make_profile(profile_session, owner=_OWNER)
|
||
client = self._make_failing_client(profile_engine, profile_session)
|
||
try:
|
||
resp = client.put(f"/api/imap-profiles/{prof.id}", json={"name": "New"})
|
||
finally:
|
||
from app.main import app
|
||
|
||
app.dependency_overrides.clear()
|
||
assert resp.status_code == 500
|
||
|
||
def test_delete_db_error_returns_500(self, profile_engine, profile_session):
|
||
"""A DB error during delete triggers rollback and returns 500."""
|
||
prof = _make_profile(profile_session, owner=_OWNER)
|
||
client = self._make_failing_client(profile_engine, profile_session, fail_on="delete")
|
||
try:
|
||
resp = client.delete(f"/api/imap-profiles/{prof.id}")
|
||
finally:
|
||
from app.main import app
|
||
|
||
app.dependency_overrides.clear()
|
||
assert resp.status_code == 500
|