test: add comprehensive tests across modules to increase coverage above 60%

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-09 11:57:09 +00:00
parent d6e57c8b47
commit c3bfb26c73
32 changed files with 2019 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
"""Tests for app/api/common.py module."""
import os
import pytest
from unittest.mock import patch
from app.api.common import resolve_file_path
@pytest.mark.unit
class TestResolveFilePath:
"""Tests for resolve_file_path function."""
def test_relative_path_resolved_to_workdir(self):
"""Test that relative paths are resolved within workdir."""
result = resolve_file_path("test.pdf")
assert result.endswith("test.pdf")
assert os.path.isabs(result)
def test_relative_path_with_subfolder(self):
"""Test relative path with subfolder parameter."""
result = resolve_file_path("test.pdf", subfolder="processed")
assert "processed" in result
assert result.endswith("test.pdf")
def test_absolute_path_within_workdir(self, tmp_path):
"""Test absolute path within workdir is accepted."""
with patch("app.api.common.settings") as mock_settings:
mock_settings.workdir = str(tmp_path)
test_file = str(tmp_path / "test.pdf")
result = resolve_file_path(test_file)
assert result == test_file
def test_path_traversal_blocked(self, tmp_path):
"""Test that path traversal attempts are blocked."""
from fastapi import HTTPException
with patch("app.api.common.settings") as mock_settings:
mock_settings.workdir = str(tmp_path)
with pytest.raises(HTTPException) as exc_info:
resolve_file_path("../../etc/passwd")
assert exc_info.value.status_code == 400
def test_path_traversal_absolute_blocked(self, tmp_path):
"""Test that absolute paths outside workdir are blocked."""
from fastapi import HTTPException
with patch("app.api.common.settings") as mock_settings:
mock_settings.workdir = str(tmp_path)
with pytest.raises(HTTPException) as exc_info:
resolve_file_path("/etc/passwd")
assert exc_info.value.status_code == 400
+127
View File
@@ -0,0 +1,127 @@
"""Tests for app/api/logs.py module."""
import pytest
from datetime import datetime
from app.models import FileRecord, ProcessingLog
@pytest.mark.integration
class TestListProcessingLogs:
"""Tests for list_processing_logs endpoint."""
def test_list_empty_logs(self, client):
"""Test listing logs when none exist."""
response = client.get("/api/logs")
assert response.status_code == 200
assert response.json() == []
def test_list_logs_with_data(self, client, db_session):
"""Test listing logs with data in database."""
# Create a log entry
log = ProcessingLog(
task_id="test-task-123",
step_name="process_document",
status="success",
message="Test log",
)
db_session.add(log)
db_session.commit()
response = client.get("/api/logs")
assert response.status_code == 200
data = response.json()
assert len(data) >= 1
assert data[0]["task_id"] == "test-task-123"
assert data[0]["step_name"] == "process_document"
assert data[0]["status"] == "success"
def test_list_logs_filter_by_task_id(self, client, db_session):
"""Test filtering logs by task_id."""
log1 = ProcessingLog(task_id="task-a", step_name="step1", status="success", message="Log A")
log2 = ProcessingLog(task_id="task-b", step_name="step2", status="success", message="Log B")
db_session.add_all([log1, log2])
db_session.commit()
response = client.get("/api/logs?task_id=task-a")
assert response.status_code == 200
data = response.json()
assert all(log["task_id"] == "task-a" for log in data)
def test_list_logs_with_limit(self, client, db_session):
"""Test limiting number of returned logs."""
for i in range(5):
log = ProcessingLog(task_id=f"task-{i}", step_name="step", status="success", message=f"Log {i}")
db_session.add(log)
db_session.commit()
response = client.get("/api/logs?limit=2")
assert response.status_code == 200
data = response.json()
assert len(data) == 2
@pytest.mark.integration
class TestGetFileProcessingLogs:
"""Tests for get_file_processing_logs endpoint."""
def test_get_logs_for_existing_file(self, client, db_session):
"""Test getting logs for a file that exists."""
file_record = FileRecord(
filehash="abc123def456",
original_filename="test.pdf",
local_filename="/tmp/test.pdf",
file_size=1024,
mime_type="application/pdf",
)
db_session.add(file_record)
db_session.commit()
log = ProcessingLog(
file_id=file_record.id,
task_id="task-123",
step_name="process_document",
status="success",
message="Processed",
)
db_session.add(log)
db_session.commit()
response = client.get(f"/api/logs/file/{file_record.id}")
assert response.status_code == 200
data = response.json()
assert "file" in data
assert "logs" in data
assert data["file"]["original_filename"] == "test.pdf"
assert len(data["logs"]) == 1
def test_get_logs_for_nonexistent_file(self, client):
"""Test getting logs for a file that doesn't exist."""
response = client.get("/api/logs/file/99999")
assert response.status_code == 404
@pytest.mark.integration
class TestGetTaskProcessingLogs:
"""Tests for get_task_processing_logs endpoint."""
def test_get_logs_for_existing_task(self, client, db_session):
"""Test getting logs for an existing task."""
log = ProcessingLog(
task_id="test-task-abc",
step_name="process_document",
status="success",
message="Done",
)
db_session.add(log)
db_session.commit()
response = client.get("/api/logs/task/test-task-abc")
assert response.status_code == 200
data = response.json()
assert data["task_id"] == "test-task-abc"
assert len(data["logs"]) == 1
def test_get_logs_for_nonexistent_task(self, client):
"""Test getting logs for a task that doesn't exist."""
response = client.get("/api/logs/task/nonexistent-task")
assert response.status_code == 404
+21
View File
@@ -0,0 +1,21 @@
"""Tests for app/api/openai.py module."""
import pytest
@pytest.mark.integration
class TestOpenAIEndpoints:
"""Tests for OpenAI API endpoints."""
def test_test_openai_connection(self, client):
"""Test the OpenAI connection test endpoint."""
response = client.get("/api/openai/test")
assert response.status_code == 200
data = response.json()
assert "status" in data
def test_openai_test_returns_status(self, client):
"""Test that OpenAI test returns appropriate status."""
response = client.get("/api/openai/test")
data = response.json()
# Should be success or error depending on API key validity
assert data["status"] in ("success", "error")
+38
View File
@@ -0,0 +1,38 @@
"""Tests for app/api/settings.py module."""
import pytest
from unittest.mock import patch, MagicMock
from fastapi import HTTPException
from app.api.settings import require_admin
@pytest.mark.unit
class TestRequireAdmin:
"""Tests for require_admin dependency."""
def test_raises_403_when_no_user(self):
"""Test that 403 is raised when no user in session."""
mock_request = MagicMock()
mock_request.session = {}
with pytest.raises(HTTPException) as exc_info:
require_admin(mock_request)
assert exc_info.value.status_code == 403
def test_raises_403_when_not_admin(self):
"""Test that 403 is raised for non-admin user."""
mock_request = MagicMock()
mock_request.session = {"user": {"id": "1", "is_admin": False}}
with pytest.raises(HTTPException) as exc_info:
require_admin(mock_request)
assert exc_info.value.status_code == 403
def test_returns_user_when_admin(self):
"""Test that admin user is returned."""
mock_request = MagicMock()
user = {"id": "admin", "is_admin": True}
mock_request.session = {"user": user}
result = require_admin(mock_request)
assert result == user
+66
View File
@@ -0,0 +1,66 @@
"""Tests for app/api/user.py module."""
import pytest
from hashlib import md5
from unittest.mock import MagicMock
from app.api.user import whoami_handler
@pytest.mark.unit
class TestWhoamiHandler:
"""Tests for whoami_handler function."""
@pytest.mark.asyncio
async def test_returns_user_with_gravatar(self):
"""Test that handler returns user data with gravatar URL."""
mock_request = MagicMock()
email = "test@example.com"
mock_request.session = {"user": {"id": "1", "email": email, "name": "Test"}}
result = await whoami_handler(mock_request)
assert result["id"] == "1"
assert result["name"] == "Test"
# Should have gravatar URL
expected_hash = md5(email.encode(), usedforsecurity=False).hexdigest()
assert result["picture"] == f"https://www.gravatar.com/avatar/{expected_hash}?d=identicon"
@pytest.mark.asyncio
async def test_raises_401_when_no_user(self):
"""Test that 401 is raised when no user in session."""
from fastapi import HTTPException
mock_request = MagicMock()
mock_request.session = {}
with pytest.raises(HTTPException) as exc_info:
await whoami_handler(mock_request)
assert exc_info.value.status_code == 401
@pytest.mark.asyncio
async def test_raises_400_when_no_email(self):
"""Test that 400 is raised when user has no email."""
from fastapi import HTTPException
mock_request = MagicMock()
mock_request.session = {"user": {"id": "1", "name": "Test"}}
with pytest.raises(HTTPException) as exc_info:
await whoami_handler(mock_request)
assert exc_info.value.status_code == 400
@pytest.mark.integration
class TestWhoamiEndpoints:
"""Tests for whoami API endpoints."""
def test_whoami_endpoint(self, client):
"""Test /api/whoami endpoint."""
response = client.get("/api/whoami")
# Without session user, should return 401
assert response.status_code == 401
def test_auth_whoami_endpoint(self, client):
"""Test /api/auth/whoami endpoint."""
response = client.get("/api/auth/whoami")
# When auth is disabled, returns 200 with error message (no user in session)
assert response.status_code == 200
+86
View File
@@ -0,0 +1,86 @@
"""Tests for app/auth.py module."""
import hashlib
import pytest
from unittest.mock import MagicMock, AsyncMock, patch
from starlette.testclient import TestClient
from fastapi import Request
from app.auth import get_current_user, get_gravatar_url, require_login
@pytest.mark.unit
class TestGetCurrentUser:
"""Tests for get_current_user function."""
def test_returns_user_from_session(self):
"""Test that get_current_user returns user data from session."""
mock_request = MagicMock(spec=Request)
mock_request.session = {"user": {"id": "test_user", "name": "Test"}}
result = get_current_user(mock_request)
assert result == {"id": "test_user", "name": "Test"}
def test_returns_none_when_no_user(self):
"""Test that get_current_user returns None when no user in session."""
mock_request = MagicMock(spec=Request)
mock_request.session = {}
result = get_current_user(mock_request)
assert result is None
@pytest.mark.unit
class TestGetGravatarUrl:
"""Tests for get_gravatar_url function."""
def test_generates_correct_url(self):
"""Test gravatar URL generation with known email."""
email = "test@example.com"
expected_hash = hashlib.md5(email.lower().strip().encode("utf-8"), usedforsecurity=False).hexdigest()
result = get_gravatar_url(email)
assert result == f"https://www.gravatar.com/avatar/{expected_hash}?d=identicon"
def test_handles_uppercase_email(self):
"""Test that email is lowercased."""
result_upper = get_gravatar_url("TEST@EXAMPLE.COM")
result_lower = get_gravatar_url("test@example.com")
assert result_upper == result_lower
def test_handles_whitespace(self):
"""Test that whitespace is stripped."""
result_spaces = get_gravatar_url(" test@example.com ")
result_clean = get_gravatar_url("test@example.com")
assert result_spaces == result_clean
@pytest.mark.unit
class TestRequireLogin:
"""Tests for require_login decorator."""
def test_noop_when_auth_disabled(self):
"""Test that require_login is a no-op when AUTH_ENABLED is False."""
# AUTH_ENABLED is False in test environment
def my_func():
return "hello"
decorated = require_login(my_func)
# When AUTH_ENABLED is False, the decorator returns the function unchanged
assert decorated is my_func
@pytest.mark.integration
class TestWhoamiEndpoint:
"""Tests for the /api/auth/whoami endpoint."""
def test_whoami_returns_user_or_error(self, client):
"""Test whoami endpoint without auth (auth disabled)."""
response = client.get("/api/auth/whoami")
assert response.status_code == 200
data = response.json()
# When auth is disabled and no user in session, returns error dict
assert "error" in data or "id" in data
def test_private_endpoint(self, client):
"""Test /private endpoint without auth (auth disabled)."""
response = client.get("/private")
assert response.status_code == 200
data = response.json()
assert "message" in data
+91
View File
@@ -0,0 +1,91 @@
"""Integration tests for auth.py with AUTH_ENABLED=True scenarios."""
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
import hashlib
from app.auth import get_gravatar_url, get_current_user, require_login
@pytest.mark.unit
class TestRequireLoginWithAuth:
"""Tests for require_login decorator behavior."""
@pytest.mark.asyncio
async def test_require_login_redirects_when_no_user(self):
"""Test that require_login redirects to /login when no user in session."""
# Simulate AUTH_ENABLED=True by directly testing the decorator logic
with patch("app.auth.AUTH_ENABLED", True):
# Re-apply the decorator
async def my_route(request):
return {"success": True}
# Manually create the decorator behavior
from functools import wraps
import inspect
from fastapi import status
from starlette.responses import RedirectResponse
@wraps(my_route)
async def wrapper(request, *args, **kwargs):
if not request.session.get("user"):
request.session["redirect_after_login"] = str(request.url)
return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
if inspect.iscoroutinefunction(my_route):
return await my_route(request, *args, **kwargs)
else:
return my_route(request, *args, **kwargs)
mock_request = MagicMock()
mock_request.session = {}
mock_request.url = "http://localhost/upload"
result = await wrapper(mock_request)
assert result.status_code == 302
@pytest.mark.asyncio
async def test_require_login_allows_authenticated_user(self):
"""Test that require_login allows through when user exists in session."""
from functools import wraps
import inspect
from starlette.responses import RedirectResponse
from fastapi import status
async def my_route(request):
return {"success": True}
@wraps(my_route)
async def wrapper(request, *args, **kwargs):
if not request.session.get("user"):
return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
if inspect.iscoroutinefunction(my_route):
return await my_route(request, *args, **kwargs)
else:
return my_route(request, *args, **kwargs)
mock_request = MagicMock()
mock_request.session = {"user": {"id": "test", "name": "Test User"}}
result = await wrapper(mock_request)
assert result == {"success": True}
@pytest.mark.unit
class TestGravatarUrlVariations:
"""Additional tests for get_gravatar_url."""
def test_different_emails_give_different_hashes(self):
"""Test that different emails produce different URLs."""
url1 = get_gravatar_url("user1@example.com")
url2 = get_gravatar_url("user2@example.com")
assert url1 != url2
def test_url_format(self):
"""Test the URL has correct format."""
url = get_gravatar_url("test@example.com")
assert url.startswith("https://www.gravatar.com/avatar/")
assert url.endswith("?d=identicon")
def test_empty_string_email(self):
"""Test handling of empty string email."""
url = get_gravatar_url("")
assert "gravatar.com" in url
+133
View File
@@ -0,0 +1,133 @@
"""Tests for app/tasks/check_credentials.py module."""
import json
import os
import pytest
from unittest.mock import patch, MagicMock
from app.tasks.check_credentials import (
MockRequest,
get_failure_state,
save_failure_state,
unwrap_decorated_function,
)
@pytest.mark.unit
class TestMockRequest:
"""Tests for MockRequest class."""
def test_mock_request_has_session(self):
"""Test MockRequest has session attribute."""
req = MockRequest()
assert "user" in req.session
assert req.session["user"]["id"] == "credential_checker"
def test_mock_request_has_attributes(self):
"""Test MockRequest has required attributes."""
req = MockRequest()
assert req.app is None
assert isinstance(req.headers, dict)
assert isinstance(req.query_params, dict)
@pytest.mark.asyncio
async def test_mock_request_json(self):
"""Test MockRequest.json() returns empty dict."""
req = MockRequest()
result = await req.json()
assert result == {}
@pytest.mark.asyncio
async def test_mock_request_form(self):
"""Test MockRequest.form() returns empty dict."""
req = MockRequest()
result = await req.form()
assert result == {}
@pytest.mark.unit
class TestGetFailureState:
"""Tests for get_failure_state function."""
@patch("app.tasks.check_credentials.FAILURE_STATE_FILE", "/tmp/test_failure_state.json")
def test_returns_empty_dict_when_no_file(self):
"""Test returns empty dict when file doesn't exist."""
# Ensure file doesn't exist
if os.path.exists("/tmp/test_failure_state.json"):
os.remove("/tmp/test_failure_state.json")
result = get_failure_state()
assert result == {}
@patch("app.tasks.check_credentials.FAILURE_STATE_FILE", "/tmp/test_failure_state.json")
def test_reads_existing_state(self):
"""Test reads state from existing file."""
state = {"OpenAI": {"count": 2, "last_notified": 12345}}
with open("/tmp/test_failure_state.json", "w") as f:
json.dump(state, f)
result = get_failure_state()
assert result == state
# Clean up
os.remove("/tmp/test_failure_state.json")
@pytest.mark.unit
class TestSaveFailureState:
"""Tests for save_failure_state function."""
@patch("app.tasks.check_credentials.FAILURE_STATE_FILE", "/tmp/test_failure_state_save.json")
def test_saves_state_to_file(self):
"""Test saves state to file."""
state = {"OpenAI": {"count": 1, "last_notified": 0}}
save_failure_state(state)
with open("/tmp/test_failure_state_save.json", "r") as f:
loaded = json.load(f)
assert loaded == state
# Clean up
os.remove("/tmp/test_failure_state_save.json")
@pytest.mark.unit
class TestUnwrapDecoratedFunction:
"""Tests for unwrap_decorated_function."""
def test_returns_same_function_if_not_decorated(self):
"""Test returns same function if not decorated."""
def my_func():
return "hello"
result = unwrap_decorated_function(my_func)
assert result is my_func
def test_unwraps_decorated_function(self):
"""Test unwraps decorated function."""
def inner():
return "hello"
def wrapper():
return inner()
wrapper.__wrapped__ = inner
result = unwrap_decorated_function(wrapper)
assert result is inner
def test_unwraps_multiple_levels(self):
"""Test unwraps multiple levels of decoration."""
def original():
return "hello"
def middle():
return original()
middle.__wrapped__ = original
def outer():
return middle()
outer.__wrapped__ = middle
result = unwrap_decorated_function(outer)
assert result is original
+59
View File
@@ -0,0 +1,59 @@
"""Tests for app/utils/config_loader.py module."""
import pytest
from app.utils.config_loader import convert_setting_value
@pytest.mark.unit
class TestConvertSettingValue:
"""Tests for convert_setting_value function."""
def test_converts_bool_true_string(self):
"""Test converting 'true' string to boolean."""
result = convert_setting_value("true", bool)
assert result is True
def test_converts_bool_false_string(self):
"""Test converting 'false' string to boolean."""
result = convert_setting_value("false", bool)
assert result is False
def test_converts_bool_yes(self):
"""Test converting 'yes' to boolean."""
result = convert_setting_value("yes", bool)
assert result is True
def test_converts_integer_string(self):
"""Test converting integer string."""
result = convert_setting_value("42", int)
assert result == 42
def test_converts_invalid_integer(self):
"""Test converting invalid integer returns 0."""
result = convert_setting_value("not_a_number", int)
assert result == 0
def test_converts_float_string(self):
"""Test converting float string."""
result = convert_setting_value("3.14", float)
assert result == 3.14
def test_converts_invalid_float(self):
"""Test converting invalid float returns 0.0."""
result = convert_setting_value("not_a_float", float)
assert result == 0.0
def test_preserves_regular_string(self):
"""Test regular strings are preserved."""
result = convert_setting_value("hello world", str)
assert result == "hello world"
def test_handles_none(self):
"""Test handling of None."""
result = convert_setting_value(None, str)
assert result is None
def test_converts_list_string(self):
"""Test converting comma-separated string to list."""
result = convert_setting_value("a, b, c", list)
assert result == ["a", "b", "c"]
+70
View File
@@ -0,0 +1,70 @@
"""Tests for app/utils/config_validator/validators.py module."""
import pytest
from unittest.mock import patch
from app.utils.config_validator.validators import (
validate_storage_configs,
validate_email_config,
validate_notification_config,
check_all_configs,
)
@pytest.mark.unit
class TestValidateStorageConfigs:
"""Tests for validate_storage_configs function."""
def test_returns_dict(self):
"""Test returns a dictionary."""
result = validate_storage_configs()
assert isinstance(result, dict)
def test_has_expected_keys(self):
"""Test has expected provider keys."""
result = validate_storage_configs()
expected_keys = ["dropbox", "nextcloud", "sftp", "s3", "ftp", "webdav", "google_drive", "onedrive"]
for key in expected_keys:
assert key in result
def test_values_are_lists(self):
"""Test that values are lists of issues."""
result = validate_storage_configs()
for key, issues in result.items():
assert isinstance(issues, list)
@pytest.mark.unit
class TestValidateEmailConfig:
"""Tests for validate_email_config function."""
def test_returns_list(self):
"""Test returns a list."""
result = validate_email_config()
assert isinstance(result, list)
@pytest.mark.unit
class TestValidateNotificationConfig:
"""Tests for validate_notification_config function."""
def test_returns_list(self):
"""Test returns a list."""
result = validate_notification_config()
assert isinstance(result, list)
@pytest.mark.unit
class TestCheckAllConfigs:
"""Tests for check_all_configs function."""
def test_returns_dict(self):
"""Test returns a dictionary."""
result = check_all_configs()
assert isinstance(result, dict)
def test_has_expected_keys(self):
"""Test has expected keys."""
result = check_all_configs()
assert "storage" in result
assert "email" in result
assert "notification" in result
+50
View File
@@ -0,0 +1,50 @@
"""Tests for app/tasks/convert_to_pdf.py module."""
import os
import pytest
from unittest.mock import patch, MagicMock
@pytest.mark.unit
class TestConvertToPdfMimeTypes:
"""Tests for file type detection in convert_to_pdf."""
def test_office_extensions_set(self):
"""Test that the task module is importable."""
from app.tasks.convert_to_pdf import convert_to_pdf
assert callable(convert_to_pdf)
@patch("app.tasks.convert_to_pdf.requests")
@patch("app.tasks.convert_to_pdf.process_document")
@patch("app.tasks.convert_to_pdf.log_task_progress")
def test_convert_with_no_gotenberg_url(self, mock_log, mock_process, mock_requests):
"""Test convert_to_pdf when gotenberg_url is not configured."""
with patch("app.tasks.convert_to_pdf.settings") as mock_settings:
mock_settings.gotenberg_url = None
mock_self = MagicMock()
mock_self.request.id = "test-task-id"
from app.tasks.convert_to_pdf import convert_to_pdf
result = convert_to_pdf.__wrapped__(mock_self, "/tmp/test.docx")
assert result is None
@patch("app.tasks.convert_to_pdf.requests")
@patch("app.tasks.convert_to_pdf.process_document")
@patch("app.tasks.convert_to_pdf.log_task_progress")
def test_convert_exception_handling(self, mock_log, mock_process, mock_requests, tmp_path):
"""Test convert_to_pdf exception handling."""
test_file = tmp_path / "test.docx"
test_file.write_bytes(b"test content")
mock_requests.post.side_effect = Exception("Connection error")
with patch("app.tasks.convert_to_pdf.settings") as mock_settings:
mock_settings.gotenberg_url = "http://localhost:3000"
mock_settings.http_request_timeout = 30
mock_self = MagicMock()
mock_self.request.id = "test-task-id"
from app.tasks.convert_to_pdf import convert_to_pdf
result = convert_to_pdf.__wrapped__(mock_self, str(test_file))
assert result is None
+72
View File
@@ -0,0 +1,72 @@
"""Tests for app/database.py module."""
import os
import pytest
from unittest.mock import patch, MagicMock
from app.database import init_db, get_db
@pytest.mark.unit
class TestInitDb:
"""Tests for init_db function."""
def test_init_db_creates_tables(self):
"""Test that init_db creates tables without error."""
# In test environment, DATABASE_URL is sqlite:///:memory:
init_db()
@patch("app.database.make_url")
@patch("app.database.Base")
def test_init_db_with_sqlite_file(self, mock_base, mock_make_url, tmp_path):
"""Test init_db with a file-based SQLite database."""
db_path = str(tmp_path / "test_db" / "test.db")
mock_url = MagicMock()
mock_url.get_backend_name.return_value = "sqlite"
mock_url.database = db_path
mock_make_url.return_value = mock_url
init_db()
@patch("app.database.make_url")
@patch("app.database.Base")
def test_init_db_with_memory_db(self, mock_base, mock_make_url):
"""Test init_db with in-memory database."""
mock_url = MagicMock()
mock_url.get_backend_name.return_value = "sqlite"
mock_url.database = ":memory:"
mock_make_url.return_value = mock_url
init_db()
@patch("app.database.make_url")
@patch("app.database.Base")
def test_init_db_with_non_sqlite(self, mock_base, mock_make_url):
"""Test init_db with non-SQLite database."""
mock_url = MagicMock()
mock_url.get_backend_name.return_value = "postgresql"
mock_make_url.return_value = mock_url
init_db()
@pytest.mark.unit
class TestGetDb:
"""Tests for get_db function."""
def test_get_db_yields_session(self):
"""Test that get_db yields a session and closes it."""
gen = get_db()
session = next(gen)
assert session is not None
# Clean up
try:
next(gen)
except StopIteration:
pass
def test_get_db_closes_session_on_exit(self):
"""Test that the session is closed when exiting the generator."""
gen = get_db()
session = next(gen)
# Force the generator to close
gen.close()
+38
View File
@@ -0,0 +1,38 @@
"""Tests for app/api/diagnostic.py module."""
import pytest
@pytest.mark.integration
class TestDiagnosticSettings:
"""Tests for diagnostic settings endpoint."""
def test_diagnostic_settings_endpoint(self, client):
"""Test /api/diagnostic/settings endpoint."""
response = client.get("/api/diagnostic/settings")
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
assert "settings" in data
assert "configured_services" in data["settings"]
def test_diagnostic_settings_has_expected_services(self, client):
"""Test that diagnostic settings has expected service keys."""
response = client.get("/api/diagnostic/settings")
data = response.json()
services = data["settings"]["configured_services"]
expected_keys = ["email", "s3", "dropbox", "onedrive", "nextcloud", "sftp", "openai", "azure"]
for key in expected_keys:
assert key in services
@pytest.mark.integration
class TestTestNotification:
"""Tests for test notification endpoint."""
def test_test_notification_endpoint(self, client):
"""Test /api/diagnostic/test-notification endpoint."""
response = client.post("/api/diagnostic/test-notification")
assert response.status_code == 200
data = response.json()
# Should return warning (no notification services configured) or success
assert data["status"] in ("warning", "success", "error")
+58
View File
@@ -0,0 +1,58 @@
"""Tests for app/tasks/embed_metadata_into_pdf.py module."""
import os
import pytest
from unittest.mock import patch, MagicMock
from app.tasks.embed_metadata_into_pdf import unique_filepath, persist_metadata
@pytest.mark.unit
class TestUniqueFilepath:
"""Tests for unique_filepath function."""
def test_returns_path_when_no_conflict(self, tmp_path):
"""Test returns original path when no conflict."""
result = unique_filepath(str(tmp_path), "test", ".pdf")
assert result == str(tmp_path / "test.pdf")
def test_appends_counter_on_conflict(self, tmp_path):
"""Test appends counter when file already exists."""
# Create the initial file
(tmp_path / "test.pdf").touch()
result = unique_filepath(str(tmp_path), "test", ".pdf")
assert result == str(tmp_path / "test_1.pdf")
def test_increments_counter(self, tmp_path):
"""Test increments counter for multiple conflicts."""
(tmp_path / "test.pdf").touch()
(tmp_path / "test_1.pdf").touch()
result = unique_filepath(str(tmp_path), "test", ".pdf")
assert result == str(tmp_path / "test_2.pdf")
@pytest.mark.unit
class TestPersistMetadata:
"""Tests for persist_metadata function."""
def test_saves_metadata_as_json(self, tmp_path):
"""Test that metadata is saved as JSON file."""
import json
pdf_path = str(tmp_path / "test.pdf")
metadata = {"document_type": "invoice", "tags": ["test"]}
result = persist_metadata(metadata, pdf_path)
assert result == str(tmp_path / "test.json")
assert os.path.exists(result)
with open(result) as f:
loaded = json.load(f)
assert loaded == metadata
def test_json_filename_matches_pdf(self, tmp_path):
"""Test that JSON filename matches PDF filename."""
pdf_path = str(tmp_path / "2024-01-01_Invoice.pdf")
metadata = {"title": "Invoice"}
result = persist_metadata(metadata, pdf_path)
assert result.endswith("2024-01-01_Invoice.json")
+40
View File
@@ -0,0 +1,40 @@
"""Tests for app/tasks/extract_metadata_with_gpt.py module."""
import pytest
from app.tasks.extract_metadata_with_gpt import extract_json_from_text
@pytest.mark.unit
class TestExtractJsonFromText:
"""Tests for extract_json_from_text function."""
def test_extracts_json_from_backticks(self):
"""Test extraction of JSON from triple-backtick block."""
text = '```json\n{"key": "value"}\n```'
result = extract_json_from_text(text)
assert result == '{"key": "value"}'
def test_extracts_json_from_backticks_no_lang(self):
"""Test extraction from backticks without language tag."""
text = '```\n{"key": "value"}\n```'
result = extract_json_from_text(text)
assert result == '{"key": "value"}'
def test_extracts_json_from_raw_text(self):
"""Test extraction from raw text with JSON."""
text = 'Here is the result: {"key": "value"} end.'
result = extract_json_from_text(text)
assert result == '{"key": "value"}'
def test_returns_none_for_no_json(self):
"""Test returns None when no JSON found."""
text = "No JSON here at all."
result = extract_json_from_text(text)
assert result is None
def test_extracts_complex_json(self):
"""Test extraction of complex JSON."""
text = '{"filename": "2024-01-01_Invoice", "tags": ["test", "invoice"], "amount": 100}'
result = extract_json_from_text(text)
assert '"filename"' in result
assert '"tags"' in result
+21
View File
@@ -0,0 +1,21 @@
"""Tests for app/tasks/finalize_document_storage.py module."""
import os
import pytest
from unittest.mock import patch, MagicMock
@pytest.mark.unit
class TestFinalizeDocumentStorageHelpers:
"""Tests for helper functions used in finalize_document_storage."""
def test_get_configured_services_from_validator(self):
"""Test that get_configured_services_from_validator returns a dict."""
from app.tasks.send_to_all import get_configured_services_from_validator
result = get_configured_services_from_validator()
assert isinstance(result, dict)
def test_module_imports(self):
"""Test that the module can be imported without errors."""
from app.tasks.finalize_document_storage import finalize_document_storage
assert callable(finalize_document_storage)
+280
View File
@@ -0,0 +1,280 @@
"""Tests for app/tasks/imap_tasks.py module."""
import os
import json
import pytest
from datetime import datetime, timedelta, timezone
from unittest.mock import patch, MagicMock
from email.message import EmailMessage
from app.tasks.imap_tasks import (
load_processed_emails,
save_processed_emails,
cleanup_old_entries,
check_and_pull_mailbox,
fetch_attachments_and_enqueue,
email_already_has_label,
mark_as_processed_with_star,
mark_as_processed_with_label,
find_all_mail_folder,
get_capabilities,
)
@pytest.mark.unit
class TestCleanupOldEntries:
"""Tests for cleanup_old_entries function."""
def test_removes_old_entries(self):
"""Test that entries older than 7 days are removed."""
old_date = (datetime.now(timezone.utc) - timedelta(days=10)).strftime("%Y-%m-%dT%H:%M:%S")
recent_date = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
processed = {"old-msg": old_date, "recent-msg": recent_date}
result = cleanup_old_entries(processed)
assert "old-msg" not in result
assert "recent-msg" in result
def test_keeps_recent_entries(self):
"""Test that entries within 7 days are kept."""
recent_date = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
processed = {"msg-1": recent_date, "msg-2": recent_date}
result = cleanup_old_entries(processed)
assert len(result) == 2
def test_empty_dict(self):
"""Test with empty dictionary."""
result = cleanup_old_entries({})
assert result == {}
@pytest.mark.unit
class TestLoadSaveProcessedEmails:
"""Tests for load_processed_emails and save_processed_emails."""
@patch("app.tasks.imap_tasks.CACHE_FILE", "/tmp/test_processed_mails.json")
def test_load_returns_empty_when_no_file(self):
"""Test load returns empty dict when file doesn't exist."""
if os.path.exists("/tmp/test_processed_mails.json"):
os.remove("/tmp/test_processed_mails.json")
result = load_processed_emails()
assert result == {}
@patch("app.tasks.imap_tasks.CACHE_FILE", "/tmp/test_processed_mails.json")
def test_save_and_load_roundtrip(self):
"""Test saving and loading processed emails."""
recent_date = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
emails = {"msg-1": recent_date}
save_processed_emails(emails)
result = load_processed_emails()
assert "msg-1" in result
# Clean up
os.remove("/tmp/test_processed_mails.json")
@pytest.mark.unit
class TestCheckAndPullMailbox:
"""Tests for check_and_pull_mailbox function."""
@patch("app.tasks.imap_tasks.pull_inbox")
def test_skips_when_no_host(self, mock_pull):
"""Test that it skips when host is not configured."""
check_and_pull_mailbox(
mailbox_key="imap1",
host=None,
port=993,
username="user",
password="pass",
use_ssl=True,
delete_after_process=False,
)
mock_pull.assert_not_called()
@patch("app.tasks.imap_tasks.pull_inbox")
def test_skips_when_no_password(self, mock_pull):
"""Test that it skips when password is not configured."""
check_and_pull_mailbox(
mailbox_key="imap1",
host="imap.example.com",
port=993,
username="user",
password=None,
use_ssl=True,
delete_after_process=False,
)
mock_pull.assert_not_called()
@patch("app.tasks.imap_tasks.pull_inbox")
def test_calls_pull_inbox_when_configured(self, mock_pull):
"""Test that it calls pull_inbox when properly configured."""
check_and_pull_mailbox(
mailbox_key="imap1",
host="imap.example.com",
port=993,
username="user",
password="pass",
use_ssl=True,
delete_after_process=False,
)
mock_pull.assert_called_once()
@pytest.mark.unit
class TestFetchAttachmentsAndEnqueue:
"""Tests for fetch_attachments_and_enqueue function."""
@patch("app.tasks.imap_tasks.process_document")
@patch("app.tasks.imap_tasks.convert_to_pdf")
def test_skips_non_allowed_mime_types(self, mock_convert, mock_process):
"""Test that non-allowed MIME types are skipped."""
msg = EmailMessage()
msg["Subject"] = "Test"
msg.add_attachment(b"data", maintype="application", subtype="octet-stream", filename="test.exe")
result = fetch_attachments_and_enqueue(msg)
assert result is False
mock_process.delay.assert_not_called()
mock_convert.delay.assert_not_called()
@patch("app.tasks.imap_tasks.process_document")
@patch("app.tasks.imap_tasks.convert_to_pdf")
def test_processes_pdf_attachment(self, mock_convert, mock_process, tmp_path):
"""Test that PDF attachments are processed directly."""
msg = EmailMessage()
msg["Subject"] = "Test PDF"
msg.add_attachment(b"%PDF-1.4", maintype="application", subtype="pdf", filename="test.pdf")
with patch("app.tasks.imap_tasks.settings") as mock_settings:
mock_settings.workdir = str(tmp_path)
result = fetch_attachments_and_enqueue(msg)
assert result is True
mock_process.delay.assert_called_once()
@patch("app.tasks.imap_tasks.process_document")
@patch("app.tasks.imap_tasks.convert_to_pdf")
def test_converts_docx_attachment(self, mock_convert, mock_process, tmp_path):
"""Test that DOCX attachments are sent for conversion."""
msg = EmailMessage()
msg["Subject"] = "Test DOCX"
msg.add_attachment(
b"docx content",
maintype="application",
subtype="vnd.openxmlformats-officedocument.wordprocessingml.document",
filename="test.docx",
)
with patch("app.tasks.imap_tasks.settings") as mock_settings:
mock_settings.workdir = str(tmp_path)
result = fetch_attachments_and_enqueue(msg)
assert result is True
mock_convert.delay.assert_called_once()
@pytest.mark.unit
class TestEmailAlreadyHasLabel:
"""Tests for email_already_has_label function."""
def test_returns_true_when_label_found(self):
"""Test returns True when label is found."""
mock_mail = MagicMock()
mock_mail.fetch.return_value = ("OK", [(None, b'"Ingested" "INBOX"')])
result = email_already_has_label(mock_mail, b"1", "Ingested")
assert result is True
def test_returns_false_when_label_not_found(self):
"""Test returns False when label is not found."""
mock_mail = MagicMock()
mock_mail.fetch.return_value = ("OK", [(None, b'"INBOX" "Sent"')])
result = email_already_has_label(mock_mail, b"1", "Ingested")
assert result is False
def test_handles_exception(self):
"""Test handles exception gracefully."""
mock_mail = MagicMock()
mock_mail.fetch.side_effect = Exception("IMAP error")
result = email_already_has_label(mock_mail, b"1", "Ingested")
assert result is False
@pytest.mark.unit
class TestMarkAsProcessed:
"""Tests for mark_as_processed functions."""
def test_mark_with_star(self):
"""Test marking email with star."""
mock_mail = MagicMock()
mark_as_processed_with_star(mock_mail, b"1")
mock_mail.store.assert_called_once_with(b"1", "+FLAGS", "\\Flagged")
def test_mark_with_label(self):
"""Test marking email with label."""
mock_mail = MagicMock()
mark_as_processed_with_label(mock_mail, b"1", "Ingested")
mock_mail.store.assert_called_once_with(b"1", "+X-GM-LABELS", "Ingested")
def test_mark_with_star_handles_exception(self):
"""Test mark_as_processed_with_star handles exception."""
mock_mail = MagicMock()
mock_mail.store.side_effect = Exception("IMAP error")
# Should not raise
mark_as_processed_with_star(mock_mail, b"1")
def test_mark_with_label_handles_exception(self):
"""Test mark_as_processed_with_label handles exception."""
mock_mail = MagicMock()
mock_mail.store.side_effect = Exception("IMAP error")
# Should not raise
mark_as_processed_with_label(mock_mail, b"1", "Ingested")
@pytest.mark.unit
class TestGetCapabilities:
"""Tests for get_capabilities function."""
def test_returns_capabilities(self):
"""Test returns list of capabilities."""
mock_mail = MagicMock()
mock_mail.capability.return_value = ("OK", [b"IMAP4REV1 IDLE XLIST"])
result = get_capabilities(mock_mail)
assert "IMAP4REV1" in result
assert "XLIST" in result
def test_returns_empty_on_failure(self):
"""Test returns empty list on failure."""
mock_mail = MagicMock()
mock_mail.capability.return_value = ("NO", None)
result = get_capabilities(mock_mail)
assert result == []
@pytest.mark.unit
class TestFindAllMailFolder:
"""Tests for find_all_mail_folder function."""
def test_finds_english_all_mail(self):
"""Test finding English All Mail folder."""
mock_mail = MagicMock()
# First attempt fails, second succeeds
mock_mail.select.side_effect = [
("NO", None), # German
("OK", None), # English
]
result = find_all_mail_folder(mock_mail)
assert result == "[Gmail]/All Mail"
def test_returns_none_when_not_found(self):
"""Test returns None when All Mail folder is not found."""
mock_mail = MagicMock()
mock_mail.select.return_value = ("NO", None)
mock_mail.capability.return_value = ("OK", [b"IMAP4REV1"])
result = find_all_mail_folder(mock_mail)
assert result is None
+125
View File
@@ -0,0 +1,125 @@
"""Tests for app/utils/notification.py module."""
import pytest
from unittest.mock import patch, MagicMock
from app.utils.notification import (
_mask_sensitive_url,
send_notification,
notify_celery_failure,
notify_credential_failure,
notify_startup,
notify_shutdown,
notify_file_processed,
init_apprise,
)
@pytest.mark.unit
class TestMaskSensitiveUrl:
"""Tests for _mask_sensitive_url function."""
def test_masks_basic_auth_credentials(self):
"""Test masking of basic auth credentials in URL."""
url = "smtp://user:mypassword@smtp.example.com:587"
result = _mask_sensitive_url(url)
assert "mypassword" not in result
assert "user" in result
assert "****" in result
def test_masks_discord_webhook(self):
"""Test masking of Discord webhook URL."""
url = "discord://webhook_id/webhook_token"
result = _mask_sensitive_url(url)
assert "webhook_token" not in result
def test_masks_telegram_bot_token(self):
"""Test masking of Telegram bot token."""
url = "tgram://bot_token/chat_id"
result = _mask_sensitive_url(url)
assert "chat_id" not in result
def test_masks_query_param_tokens(self):
"""Test masking of token query parameters."""
url = "https://example.com/api?token=secret123&key=apikey456"
result = _mask_sensitive_url(url)
assert "secret123" not in result
assert "apikey456" not in result
@pytest.mark.unit
class TestSendNotification:
"""Tests for send_notification function."""
@patch("app.utils.notification.settings")
def test_returns_false_when_no_urls_configured(self, mock_settings):
"""Test that send_notification returns False when no URLs are configured."""
mock_settings.notification_urls = []
result = send_notification(title="Test", message="Test message")
assert result is False
@patch("app.utils.notification.settings")
def test_returns_false_when_urls_is_none(self, mock_settings):
"""Test that send_notification returns False when URLs is None."""
mock_settings.notification_urls = None
result = send_notification(title="Test", message="Test message")
assert result is False
@pytest.mark.unit
class TestNotifyCeleryFailure:
"""Tests for notify_celery_failure function."""
@patch("app.utils.notification.settings")
def test_returns_false_when_disabled(self, mock_settings):
"""Test returns False when task failure notifications are disabled."""
mock_settings.notify_on_task_failure = False
result = notify_celery_failure("test_task", "task-123", Exception("test"), [], {})
assert result is False
@pytest.mark.unit
class TestNotifyCredentialFailure:
"""Tests for notify_credential_failure function."""
@patch("app.utils.notification.settings")
def test_returns_false_when_disabled(self, mock_settings):
"""Test returns False when credential failure notifications are disabled."""
mock_settings.notify_on_credential_failure = False
result = notify_credential_failure("OpenAI", "Invalid key")
assert result is False
@pytest.mark.unit
class TestNotifyStartup:
"""Tests for notify_startup function."""
@patch("app.utils.notification.settings")
def test_returns_false_when_disabled(self, mock_settings):
"""Test returns False when startup notifications are disabled."""
mock_settings.notify_on_startup = False
result = notify_startup()
assert result is False
@pytest.mark.unit
class TestNotifyShutdown:
"""Tests for notify_shutdown function."""
@patch("app.utils.notification.settings")
def test_returns_false_when_disabled(self, mock_settings):
"""Test returns False when shutdown notifications are disabled."""
mock_settings.notify_on_shutdown = False
result = notify_shutdown()
assert result is False
@pytest.mark.unit
class TestNotifyFileProcessed:
"""Tests for notify_file_processed function."""
@patch("app.utils.notification.settings")
def test_returns_false_when_disabled(self, mock_settings):
"""Test returns False when file processed notifications are disabled."""
mock_settings.notify_on_file_processed = False
result = notify_file_processed("test.pdf", 1024, {"document_type": "invoice"}, ["Dropbox"])
assert result is False
+43
View File
@@ -0,0 +1,43 @@
"""Tests for app/tasks/upload_with_rclone.py module."""
import os
import pytest
from unittest.mock import patch, MagicMock
from app.tasks.upload_with_rclone import upload_with_rclone
@pytest.mark.unit
class TestUploadWithRclone:
"""Tests for upload_with_rclone task."""
def test_raises_file_not_found(self):
"""Test raises FileNotFoundError for missing file."""
with pytest.raises(FileNotFoundError):
upload_with_rclone("/nonexistent/file.pdf", "remote:path")
def test_raises_value_error_invalid_destination(self, tmp_path):
"""Test raises ValueError for invalid destination format."""
test_file = tmp_path / "test.pdf"
test_file.write_bytes(b"test")
with pytest.raises(ValueError, match="Invalid destination format"):
upload_with_rclone(str(test_file), "invalid_destination")
def test_raises_value_error_invalid_remote_name(self, tmp_path):
"""Test raises ValueError for invalid remote name."""
test_file = tmp_path / "test.pdf"
test_file.write_bytes(b"test")
with pytest.raises(ValueError, match="Invalid remote name"):
upload_with_rclone(str(test_file), ":path")
def test_raises_value_error_no_config(self, tmp_path):
"""Test raises ValueError when rclone config not found."""
test_file = tmp_path / "test.pdf"
test_file.write_bytes(b"test")
with patch("app.tasks.upload_with_rclone.settings") as mock_settings:
mock_settings.workdir = str(tmp_path)
with pytest.raises(ValueError, match="Rclone configuration not found"):
upload_with_rclone(str(test_file), "gdrive:uploads")
+80
View File
@@ -0,0 +1,80 @@
"""Tests for app/utils/config_validator/settings_display.py module."""
import pytest
from unittest.mock import patch
from app.utils.config_validator.settings_display import dump_all_settings, get_settings_for_display
@pytest.mark.unit
class TestDumpAllSettings:
"""Tests for dump_all_settings function."""
def test_dumps_without_error(self):
"""Test that dump_all_settings runs without error."""
# This should not raise any exceptions
dump_all_settings()
@patch("app.utils.config_validator.settings_display.logger")
def test_logs_settings(self, mock_logger):
"""Test that dump_all_settings logs settings."""
dump_all_settings()
# Should have logged start and end markers
assert mock_logger.info.call_count > 2 # At least start, some settings, and end
@pytest.mark.unit
class TestGetSettingsForDisplay:
"""Tests for get_settings_for_display function."""
def test_returns_dict(self):
"""Test that it returns a dictionary."""
result = get_settings_for_display()
assert isinstance(result, dict)
def test_has_system_info(self):
"""Test that System Info category is present."""
result = get_settings_for_display()
assert "System Info" in result
def test_system_info_has_version(self):
"""Test that System Info includes app version."""
result = get_settings_for_display()
system_info = result["System Info"]
names = [item["name"] for item in system_info]
assert "App Version" in names
def test_has_core_settings(self):
"""Test that Core settings category is present."""
result = get_settings_for_display()
assert "Core" in result
def test_has_ai_services(self):
"""Test that AI Services category is present."""
result = get_settings_for_display()
assert "AI Services" in result
def test_show_values_false_masks_sensitive(self):
"""Test that sensitive values are masked when show_values is False."""
result = get_settings_for_display(show_values=False)
# Check that AI Services settings have masked values
if "AI Services" in result:
for item in result["AI Services"]:
if "key" in item["name"].lower() or "token" in item["name"].lower():
if item["value"]:
# Sensitive values should be masked
assert "****" in str(item["value"]) or isinstance(item["value"], str)
def test_show_values_true(self):
"""Test that settings are returned with show_values=True."""
result = get_settings_for_display(show_values=True)
assert isinstance(result, dict)
assert len(result) > 0
def test_items_have_required_keys(self):
"""Test that each setting item has required keys."""
result = get_settings_for_display()
for category, items in result.items():
for item in items:
assert "name" in item
assert "value" in item
assert "is_configured" in item
+121
View File
@@ -0,0 +1,121 @@
"""Tests for app/utils/setup_wizard.py module."""
import pytest
from unittest.mock import patch
from app.utils.setup_wizard import (
get_required_settings,
is_setup_required,
get_missing_required_settings,
get_wizard_steps,
)
@pytest.mark.unit
class TestGetRequiredSettings:
"""Tests for get_required_settings function."""
def test_returns_list(self):
"""Test that it returns a list."""
result = get_required_settings()
assert isinstance(result, list)
assert len(result) > 0
def test_each_setting_has_required_keys(self):
"""Test that each setting has the expected keys."""
required_keys = {"key", "label", "description", "type", "sensitive", "wizard_step", "wizard_category"}
for setting in get_required_settings():
assert required_keys.issubset(set(setting.keys())), f"Missing keys in {setting.get('key', 'unknown')}"
def test_includes_critical_settings(self):
"""Test that critical settings are included."""
keys = [s["key"] for s in get_required_settings()]
assert "database_url" in keys
assert "redis_url" in keys
assert "session_secret" in keys
assert "openai_api_key" in keys
@pytest.mark.unit
class TestIsSetupRequired:
"""Tests for is_setup_required function."""
def test_returns_boolean(self):
"""Test that is_setup_required returns a boolean."""
result = is_setup_required()
assert isinstance(result, bool)
def test_setup_required_with_test_key(self):
"""Test that setup is required when using test-key placeholder."""
# In test environment, openai_api_key is "test-key" which is a placeholder
result = is_setup_required()
assert result is True
@patch("app.utils.setup_wizard.settings")
def test_setup_not_required_with_real_values(self, mock_settings):
"""Test that setup is not required with real values."""
mock_settings.session_secret = "a_very_long_real_session_secret_that_is_definitely_not_placeholder"
mock_settings.admin_password = "my_real_secure_password_123"
mock_settings.openai_api_key = "sk-real-key-12345"
mock_settings.azure_ai_key = "real-azure-key-12345"
result = is_setup_required()
assert result is False
@patch("app.utils.setup_wizard.settings")
def test_handles_exception_gracefully(self, mock_settings):
"""Test that exceptions are handled gracefully."""
mock_settings.session_secret = property(lambda self: (_ for _ in ()).throw(Exception("test")))
# getattr on a mock with side_effect
type(mock_settings).session_secret = property(lambda s: (_ for _ in ()).throw(RuntimeError("boom")))
# This should not raise - it returns False on error
result = is_setup_required()
# May return True or False depending on which setting fails, but shouldn't raise
assert isinstance(result, bool)
@pytest.mark.unit
class TestGetMissingRequiredSettings:
"""Tests for get_missing_required_settings function."""
def test_returns_list(self):
"""Test that it returns a list."""
result = get_missing_required_settings()
assert isinstance(result, list)
def test_includes_placeholder_settings(self):
"""Test that settings with placeholder values are included."""
missing = get_missing_required_settings()
# In test environment, openai_api_key is "test-key" which is a placeholder
assert "openai_api_key" in missing
@pytest.mark.unit
class TestGetWizardSteps:
"""Tests for get_wizard_steps function."""
def test_returns_dict(self):
"""Test that it returns a dictionary."""
result = get_wizard_steps()
assert isinstance(result, dict)
def test_steps_are_numbered(self):
"""Test that steps are numbered starting from 1."""
steps = get_wizard_steps()
assert 1 in steps
def test_each_step_has_settings(self):
"""Test that each step has a list of settings."""
steps = get_wizard_steps()
for step_num, settings_list in steps.items():
assert isinstance(settings_list, list)
assert len(settings_list) > 0
def test_steps_cover_all_required_settings(self):
"""Test that all required settings are assigned to a step."""
steps = get_wizard_steps()
all_step_keys = []
for settings_list in steps.values():
all_step_keys.extend([s["key"] for s in settings_list])
required_keys = [s["key"] for s in get_required_settings()]
for key in required_keys:
assert key in all_step_keys, f"Setting {key} not assigned to any wizard step"
+13
View File
@@ -0,0 +1,13 @@
"""Tests for app/tasks/upload_to_email.py module."""
import pytest
from unittest.mock import patch, MagicMock
@pytest.mark.unit
class TestUploadToEmail:
"""Tests for upload_to_email task."""
def test_module_imports(self):
"""Test that the module can be imported."""
from app.tasks.upload_to_email import upload_to_email
assert callable(upload_to_email)
+13
View File
@@ -0,0 +1,13 @@
"""Additional tests for upload_to_ftp task."""
import pytest
from unittest.mock import patch, MagicMock
@pytest.mark.unit
class TestUploadToFtp:
"""Tests for upload_to_ftp task."""
def test_module_imports(self):
"""Test that the module can be imported."""
from app.tasks.upload_to_ftp import upload_to_ftp
assert callable(upload_to_ftp)
+55
View File
@@ -0,0 +1,55 @@
"""Additional tests for upload task modules."""
import os
import pytest
from unittest.mock import patch, MagicMock
@pytest.mark.unit
class TestUploadToNextcloud:
"""Tests for upload_to_nextcloud task."""
@patch("app.tasks.upload_to_nextcloud.requests")
@patch("app.tasks.upload_to_nextcloud.log_task_progress")
def test_upload_no_url_configured(self, mock_log, mock_requests):
"""Test upload when nextcloud URL is not configured."""
mock_self = MagicMock()
mock_self.request.id = "test-task"
with patch("app.tasks.upload_to_nextcloud.settings") as mock_settings:
mock_settings.nextcloud_upload_url = None
mock_settings.nextcloud_username = None
mock_settings.nextcloud_password = None
mock_settings.nextcloud_folder = None
mock_settings.workdir = "/tmp"
from app.tasks.upload_to_nextcloud import upload_to_nextcloud
result = upload_to_nextcloud.__wrapped__(mock_self, "/nonexistent/file.pdf")
@pytest.mark.unit
class TestUploadToDropbox:
"""Tests for upload_to_dropbox task."""
def test_validate_dropbox_settings_missing_all(self):
"""Test validation when all Dropbox settings are missing."""
from app.tasks.upload_to_dropbox import _validate_dropbox_settings
with patch("app.tasks.upload_to_dropbox.settings") as mock_settings:
mock_settings.dropbox_refresh_token = None
mock_settings.dropbox_app_key = None
mock_settings.dropbox_app_secret = None
result = _validate_dropbox_settings()
assert result is False
def test_get_dropbox_access_token_no_settings(self):
"""Test get_dropbox_access_token when settings are missing."""
from app.tasks.upload_to_dropbox import get_dropbox_access_token
with patch("app.tasks.upload_to_dropbox.settings") as mock_settings:
mock_settings.dropbox_refresh_token = None
mock_settings.dropbox_app_key = None
mock_settings.dropbox_app_secret = None
result = get_dropbox_access_token()
assert result is None
+27
View File
@@ -0,0 +1,27 @@
"""Tests for app/views/dropbox.py module."""
import pytest
@pytest.mark.integration
class TestDropboxViews:
"""Tests for Dropbox view routes."""
def test_dropbox_setup_page(self, client):
"""Test the Dropbox setup page."""
response = client.get("/dropbox-setup")
assert response.status_code == 200
def test_dropbox_callback_no_code(self, client):
"""Test the Dropbox OAuth callback without code."""
response = client.get("/dropbox-callback", follow_redirects=False)
assert response.status_code == 200
def test_dropbox_callback_with_error(self, client):
"""Test the Dropbox OAuth callback with error."""
response = client.get("/dropbox-callback?error=access_denied")
assert response.status_code == 200
def test_dropbox_callback_with_code(self, client):
"""Test the Dropbox OAuth callback with auth code."""
response = client.get("/dropbox-callback?code=test_code")
assert response.status_code == 200
+61
View File
@@ -0,0 +1,61 @@
"""Tests for app/views/general.py module."""
import pytest
from unittest.mock import patch, MagicMock
from pathlib import Path
@pytest.mark.integration
class TestGeneralViews:
"""Tests for general view routes."""
def test_index_page(self, client):
"""Test the index/home page returns 200 or redirects to setup."""
response = client.get("/", follow_redirects=False)
# Should either render or redirect to setup
assert response.status_code in (200, 303, 307)
def test_about_page(self, client):
"""Test the about page."""
response = client.get("/about")
assert response.status_code == 200
def test_privacy_page(self, client):
"""Test the privacy page."""
response = client.get("/privacy")
assert response.status_code == 200
def test_imprint_page(self, client):
"""Test the imprint page."""
response = client.get("/imprint")
assert response.status_code == 200
def test_upload_page(self, client):
"""Test the upload page."""
response = client.get("/upload")
assert response.status_code == 200
def test_cookies_page(self, client):
"""Test the cookies page."""
response = client.get("/cookies")
assert response.status_code == 200
def test_terms_page(self, client):
"""Test the terms page."""
response = client.get("/terms")
assert response.status_code == 200
def test_license_page(self, client):
"""Test the license page."""
response = client.get("/license")
assert response.status_code == 200
def test_favicon(self, client):
"""Test the favicon endpoint."""
response = client.get("/favicon.ico")
# May be 200 or 404 depending on whether the file exists
assert response.status_code in (200, 404)
def test_index_with_setup_complete(self, client):
"""Test index page with setup=complete query param."""
response = client.get("/?setup=complete", follow_redirects=False)
assert response.status_code == 200
+27
View File
@@ -0,0 +1,27 @@
"""Tests for app/views/google_drive.py module."""
import pytest
@pytest.mark.integration
class TestGoogleDriveViews:
"""Tests for Google Drive view routes."""
def test_google_drive_setup_page(self, client):
"""Test the Google Drive setup page."""
response = client.get("/google-drive-setup")
assert response.status_code == 200
def test_google_drive_callback_no_code(self, client):
"""Test the Google Drive OAuth callback without code."""
response = client.get("/google-drive-callback", follow_redirects=False)
assert response.status_code == 200
def test_google_drive_callback_with_error(self, client):
"""Test the Google Drive OAuth callback with error."""
response = client.get("/google-drive-callback?error=access_denied")
assert response.status_code == 200
def test_google_drive_callback_with_code(self, client):
"""Test the Google Drive OAuth callback with auth code."""
response = client.get("/google-drive-callback?code=test_code")
assert response.status_code == 200
+12
View File
@@ -0,0 +1,12 @@
"""Tests for app/views/license_routes.py module."""
import pytest
@pytest.mark.integration
class TestLicenseViews:
"""Tests for license view routes."""
def test_license_api_endpoint(self, client):
"""Test license API endpoint."""
response = client.get("/api/license")
assert response.status_code in (200, 404)
+27
View File
@@ -0,0 +1,27 @@
"""Tests for app/views/onedrive.py module."""
import pytest
@pytest.mark.integration
class TestOnedriveViews:
"""Tests for OneDrive view routes."""
def test_onedrive_setup_page(self, client):
"""Test the OneDrive setup page."""
response = client.get("/onedrive-setup")
assert response.status_code == 200
def test_onedrive_callback_no_code(self, client):
"""Test the OneDrive OAuth callback without code."""
response = client.get("/onedrive-callback", follow_redirects=False)
assert response.status_code == 200
def test_onedrive_callback_with_error(self, client):
"""Test the OneDrive OAuth callback with error."""
response = client.get("/onedrive-callback?error=access_denied")
assert response.status_code == 200
def test_onedrive_callback_with_code(self, client):
"""Test the OneDrive OAuth callback with auth code."""
response = client.get("/onedrive-callback?code=test_code")
assert response.status_code == 200
+60
View File
@@ -0,0 +1,60 @@
"""Tests for app/views/settings.py module."""
import pytest
from unittest.mock import patch, MagicMock
from app.views.settings import require_admin_access
@pytest.mark.unit
class TestRequireAdminAccess:
"""Tests for require_admin_access decorator."""
@pytest.mark.asyncio
async def test_redirects_non_admin_user(self):
"""Test that non-admin users are redirected."""
@require_admin_access
async def dummy_route(request):
return {"success": True}
mock_request = MagicMock()
mock_request.session = {"user": {"id": "1", "is_admin": False}}
result = await dummy_route(mock_request)
assert result.status_code == 302
@pytest.mark.asyncio
async def test_redirects_when_no_user(self):
"""Test that unauthenticated users are redirected."""
@require_admin_access
async def dummy_route(request):
return {"success": True}
mock_request = MagicMock()
mock_request.session = {}
result = await dummy_route(mock_request)
assert result.status_code == 302
@pytest.mark.asyncio
async def test_allows_admin_user(self):
"""Test that admin users can access the route."""
@require_admin_access
async def dummy_route(request):
return {"success": True}
mock_request = MagicMock()
mock_request.session = {"user": {"id": "admin", "is_admin": True}}
result = await dummy_route(mock_request)
assert result == {"success": True}
@pytest.mark.integration
class TestSettingsView:
"""Tests for settings page endpoint."""
def test_settings_page_redirects_without_admin(self, client):
"""Test settings page redirects non-admin users."""
response = client.get("/settings", follow_redirects=False)
# Should redirect since no user in session
assert response.status_code in (200, 302, 303)
+17
View File
@@ -0,0 +1,17 @@
"""Tests for app/views/status.py module."""
import pytest
@pytest.mark.integration
class TestStatusViews:
"""Tests for status view routes."""
def test_status_dashboard(self, client):
"""Test status dashboard page."""
response = client.get("/status")
assert response.status_code == 200
def test_env_debug_page(self, client):
"""Test env debug page."""
response = client.get("/env")
assert response.status_code == 200
+37
View File
@@ -0,0 +1,37 @@
"""Tests for app/views/wizard.py module."""
import pytest
@pytest.mark.integration
class TestWizardViews:
"""Tests for wizard view routes."""
def test_setup_wizard_step_1(self, client):
"""Test setup wizard first step."""
response = client.get("/setup?step=1")
assert response.status_code == 200
def test_setup_wizard_step_2(self, client):
"""Test setup wizard second step."""
response = client.get("/setup?step=2")
assert response.status_code == 200
def test_setup_wizard_step_3(self, client):
"""Test setup wizard third step."""
response = client.get("/setup?step=3")
assert response.status_code == 200
def test_setup_wizard_invalid_step(self, client):
"""Test setup wizard with invalid step number."""
response = client.get("/setup?step=0")
assert response.status_code == 200
def test_setup_wizard_high_step(self, client):
"""Test setup wizard with step higher than max."""
response = client.get("/setup?step=999")
assert response.status_code == 200
def test_setup_wizard_skip(self, client):
"""Test skipping the setup wizard."""
response = client.get("/setup/skip", follow_redirects=False)
assert response.status_code in (200, 303)