test(views): boost coverage for 11 view modules toward 100%
Adds tests/test_views_coverage_boost.py with 37 tests covering: - api_tokens, notifications, shared_links, share, plans views (template rendering) - imap_accounts view (helper functions + route with mocked owner) - integrations view (DB queries, tier logic, HTTP/generic exception handling) - general view (multi-user subscription branch with signed session cookie) - filemanager view (PB formatting, broken symlink stat errors in scan/walk) - files view (pipeline step filtering, dedup toggle, ValueError in commonpath) - help view (no-session branch, logged-in user Zammad widget population) Coverage improvements (full suite): - 27 of 29 view modules now at 100% (was 18 of 29) - imap_accounts: 30.95% → 100% - integrations: 82.09% → 100% - filemanager: 96.63% → 100% - help: 96% → 100% - plans: 86.67% → 100% - api_tokens/notifications/shared_links/share: 88-90% → 100% Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -4,9 +4,7 @@ Covers: api_tokens, notifications, shared_links, share, plans,
|
|||||||
imap_accounts, integrations, general, filemanager, files, help.
|
imap_accounts, integrations, general, filemanager, files, help.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
@@ -20,7 +18,6 @@ from app.config import settings as app_settings
|
|||||||
from app.database import Base, get_db
|
from app.database import Base, get_db
|
||||||
from app.main import app
|
from app.main import app
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Fixtures
|
# Fixtures
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -212,6 +209,18 @@ class TestIntegrationsView:
|
|||||||
resp = client_fresh.get("/integrations")
|
resp = client_fresh.get("/integrations")
|
||||||
assert resp.status_code == 500
|
assert resp.status_code == 500
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_integrations_dashboard_http_exception_passthrough(self, client_fresh: TestClient):
|
||||||
|
"""An HTTPException inside the dashboard is re-raised, not wrapped in 500."""
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.views.integrations.get_current_owner_id",
|
||||||
|
side_effect=HTTPException(status_code=403, detail="Forbidden"),
|
||||||
|
):
|
||||||
|
resp = client_fresh.get("/integrations")
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_get_max_destinations_free_default(self):
|
def test_get_max_destinations_free_default(self):
|
||||||
from app.views.integrations import _get_max_destinations
|
from app.views.integrations import _get_max_destinations
|
||||||
@@ -263,43 +272,65 @@ class TestIntegrationsView:
|
|||||||
class TestGeneralViewMultiUser:
|
class TestGeneralViewMultiUser:
|
||||||
"""Cover the multi_user_enabled subscription branch (lines 96-105)."""
|
"""Cover the multi_user_enabled subscription branch (lines 96-105)."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _signed_session(user_data: dict) -> str:
|
||||||
|
"""Create a signed Starlette session cookie containing user_data."""
|
||||||
|
import json
|
||||||
|
from base64 import b64encode
|
||||||
|
|
||||||
|
from itsdangerous import TimestampSigner
|
||||||
|
|
||||||
|
secret = os.environ.get(
|
||||||
|
"SESSION_SECRET",
|
||||||
|
"test_secret_key_for_testing_must_be_at_least_32_characters_long",
|
||||||
|
)
|
||||||
|
signer = TimestampSigner(secret)
|
||||||
|
data = {"user": user_data}
|
||||||
|
return signer.sign(b64encode(json.dumps(data).encode("utf-8"))).decode("utf-8")
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_home_page_multi_user_with_subscription(self, _fresh_db, client_fresh: TestClient):
|
def test_home_page_multi_user_with_subscription(self, _fresh_db, client_fresh: TestClient):
|
||||||
"""When multi_user_enabled is True and user has owner_id, subscription info is fetched."""
|
"""When multi_user_enabled is True and user has owner_id, subscription info is fetched.
|
||||||
|
|
||||||
|
Lines 96-103: Exercises the subscription lookup path.
|
||||||
|
"""
|
||||||
|
cookie_val = self._signed_session({"username": "testuser", "email": "test@example.com", "is_admin": False})
|
||||||
|
tier_mock = {
|
||||||
|
"id": "starter",
|
||||||
|
"name": "Starter",
|
||||||
|
"lifetime_file_limit": 1000,
|
||||||
|
"daily_upload_limit": 50,
|
||||||
|
"monthly_upload_limit": 500,
|
||||||
|
}
|
||||||
|
usage_mock = {"lifetime": 10, "today": 2, "month": 8}
|
||||||
with (
|
with (
|
||||||
patch.object(app_settings, "multi_user_enabled", True),
|
patch.object(app_settings, "multi_user_enabled", True),
|
||||||
patch("app.utils.setup_wizard.is_setup_required", return_value=False),
|
patch("app.utils.setup_wizard.is_setup_required", return_value=False),
|
||||||
patch("app.views.general.get_provider_status", return_value={}),
|
patch("app.views.general.get_provider_status", return_value={}),
|
||||||
patch("app.views.general.validate_storage_configs", return_value={}),
|
patch("app.views.general.validate_storage_configs", return_value={}),
|
||||||
patch(
|
patch("app.utils.subscription.get_user_tier_id", return_value="starter"),
|
||||||
"app.utils.subscription.get_user_tier_id",
|
patch("app.utils.subscription.get_tier", return_value=tier_mock),
|
||||||
return_value="starter",
|
patch("app.utils.subscription.get_user_usage", return_value=usage_mock),
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"app.utils.subscription.get_tier",
|
|
||||||
return_value={"id": "starter", "name": "Starter"},
|
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"app.utils.subscription.get_user_usage",
|
|
||||||
return_value={"pages": 10},
|
|
||||||
),
|
|
||||||
):
|
):
|
||||||
|
client_fresh.cookies.set("session", cookie_val)
|
||||||
resp = client_fresh.get("/?setup=complete")
|
resp = client_fresh.get("/?setup=complete")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_home_page_multi_user_subscription_error(self, _fresh_db, client_fresh: TestClient):
|
def test_home_page_multi_user_subscription_error(self, _fresh_db, client_fresh: TestClient):
|
||||||
"""When subscription lookup fails, error is logged but page still renders."""
|
"""When subscription lookup fails, error is logged but page still renders.
|
||||||
|
|
||||||
|
Lines 104-105: Exercises the exception handling branch.
|
||||||
|
"""
|
||||||
|
cookie_val = self._signed_session({"username": "testuser", "email": "test@example.com", "is_admin": False})
|
||||||
with (
|
with (
|
||||||
patch.object(app_settings, "multi_user_enabled", True),
|
patch.object(app_settings, "multi_user_enabled", True),
|
||||||
patch("app.utils.setup_wizard.is_setup_required", return_value=False),
|
patch("app.utils.setup_wizard.is_setup_required", return_value=False),
|
||||||
patch("app.views.general.get_provider_status", return_value={}),
|
patch("app.views.general.get_provider_status", return_value={}),
|
||||||
patch("app.views.general.validate_storage_configs", return_value={}),
|
patch("app.views.general.validate_storage_configs", return_value={}),
|
||||||
patch(
|
patch("app.utils.subscription.get_user_tier_id", side_effect=RuntimeError("boom")),
|
||||||
"app.utils.subscription.get_user_tier_id",
|
|
||||||
side_effect=RuntimeError("DB error"),
|
|
||||||
),
|
|
||||||
):
|
):
|
||||||
|
client_fresh.cookies.set("session", cookie_val)
|
||||||
resp = client_fresh.get("/?setup=complete")
|
resp = client_fresh.get("/?setup=complete")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|
||||||
@@ -523,7 +554,8 @@ class TestHelpViewCoverageGaps:
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_help_page_request_without_session(self):
|
@pytest.mark.asyncio
|
||||||
|
async def test_help_page_request_without_session(self):
|
||||||
"""Direct function call where request has no session attribute.
|
"""Direct function call where request has no session attribute.
|
||||||
|
|
||||||
Branch 34->37: when hasattr(request, 'session') is False.
|
Branch 34->37: when hasattr(request, 'session') is False.
|
||||||
@@ -537,7 +569,7 @@ class TestHelpViewCoverageGaps:
|
|||||||
|
|
||||||
with patch("app.views.help.templates") as mock_templates:
|
with patch("app.views.help.templates") as mock_templates:
|
||||||
mock_templates.TemplateResponse.return_value = "ok"
|
mock_templates.TemplateResponse.return_value = "ok"
|
||||||
result = asyncio.get_event_loop().run_until_complete(help_center(mock_request))
|
await help_center(mock_request)
|
||||||
|
|
||||||
# Template should be called with empty user context
|
# Template should be called with empty user context
|
||||||
call_args = mock_templates.TemplateResponse.call_args
|
call_args = mock_templates.TemplateResponse.call_args
|
||||||
@@ -545,3 +577,32 @@ class TestHelpViewCoverageGaps:
|
|||||||
assert ctx["user_name"] == ""
|
assert ctx["user_name"] == ""
|
||||||
assert ctx["user_email"] == ""
|
assert ctx["user_email"] == ""
|
||||||
assert ctx["user_id"] == ""
|
assert ctx["user_id"] == ""
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_help_page_with_logged_in_user(self, client_fresh: TestClient):
|
||||||
|
"""When session has user data, Zammad widget fields are populated.
|
||||||
|
|
||||||
|
Covers lines 41-43 (user_name, user_email, user_id extraction).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from base64 import b64encode
|
||||||
|
|
||||||
|
from itsdangerous import TimestampSigner
|
||||||
|
|
||||||
|
secret = os.environ.get(
|
||||||
|
"SESSION_SECRET",
|
||||||
|
"test_secret_key_for_testing_must_be_at_least_32_characters_long",
|
||||||
|
)
|
||||||
|
signer = TimestampSigner(secret)
|
||||||
|
session_data = {
|
||||||
|
"user": {
|
||||||
|
"name": "Jane Doe",
|
||||||
|
"email": "jane@example.com",
|
||||||
|
"preferred_username": "janedoe",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cookie_val = signer.sign(b64encode(json.dumps(session_data).encode("utf-8"))).decode("utf-8")
|
||||||
|
client_fresh.cookies.set("session", cookie_val)
|
||||||
|
|
||||||
|
resp = client_fresh.get("/help")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|||||||
Reference in New Issue
Block a user