Files
gh-christianlouis-docuelevate/tests/test_wizard_db_persist.py
T
2026-02-23 10:01:07 +00:00

486 lines
18 KiB
Python

"""
Tests for wizard DB persistence, settings export, and related functionality.
"""
from unittest.mock import MagicMock, patch
import pytest
from app.utils.settings_service import get_setting_from_db, save_setting_to_db
# ---------------------------------------------------------------------------
# TestSetupWizardDbPersist
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestSetupWizardDbPersist:
"""Unit tests for setup_wizard_save POST handler DB persistence."""
@patch("app.views.wizard.notify_settings_updated")
@patch("app.views.wizard.save_setting_to_db")
def test_settings_saved_to_db(self, mock_save, mock_notify, client):
"""Test that settings are saved to DB via save_setting_to_db."""
mock_save.return_value = True
response = client.post(
"/setup",
data={"step": "1", "database_url": "sqlite:///test.db"},
follow_redirects=False,
)
assert response.status_code == 303
mock_save.assert_called()
@patch("app.views.wizard.notify_settings_updated")
@patch("app.views.wizard.save_setting_to_db")
def test_notify_called_when_settings_saved(self, mock_save, mock_notify, client):
"""Test that notify_settings_updated is called when settings are saved."""
mock_save.return_value = True
client.post(
"/setup",
data={"step": "1", "database_url": "sqlite:///test.db"},
follow_redirects=False,
)
mock_notify.assert_called_once()
@patch("app.views.wizard.notify_settings_updated")
@patch("app.views.wizard.save_setting_to_db")
def test_notify_not_called_when_no_settings_saved(self, mock_save, mock_notify, client):
"""Test that notify_settings_updated is NOT called when saved_count == 0."""
mock_save.return_value = False
client.post(
"/setup",
data={"step": "1"}, # no values provided
follow_redirects=False,
)
mock_notify.assert_not_called()
@patch("app.views.wizard.notify_settings_updated")
@patch("app.views.wizard.token_hex")
@patch("app.views.wizard.save_setting_to_db")
def test_auto_generate_session_secret(self, mock_save, mock_token, mock_notify, client):
"""Test that session_secret auto-generate path produces a real token."""
mock_save.return_value = True
mock_token.return_value = "deadbeef" * 8
client.post(
"/setup",
data={"step": "2", "session_secret": "auto-generate"},
follow_redirects=False,
)
mock_token.assert_called_once()
# Ensure save was called with the generated token, not 'auto-generate'
for call_args in mock_save.call_args_list:
args = call_args[0]
if len(args) >= 2 and args[1] == "session_secret":
assert args[2] != "auto-generate"
# ---------------------------------------------------------------------------
# TestSetupWizardUndoSkip
# ---------------------------------------------------------------------------
@pytest.mark.integration
class TestSetupWizardUndoSkip:
"""Tests for /setup/undo-skip route."""
def test_undo_skip_removes_marker(self, client, db_session):
"""Test that undo-skip removes the _setup_wizard_skipped marker from DB."""
# First, put the marker in DB
save_setting_to_db(db_session, "_setup_wizard_skipped", "true")
assert get_setting_from_db(db_session, "_setup_wizard_skipped") == "true"
# Undo skip via the route
response = client.get("/setup/undo-skip", follow_redirects=False)
# Should redirect
assert response.status_code in (303, 200)
def test_undo_skip_redirects_to_wizard(self, client):
"""Test that undo-skip redirects to /setup?step=1."""
response = client.get("/setup/undo-skip", follow_redirects=False)
# The redirect should go to /setup?step=1 or /settings on error
assert response.status_code in (303, 302)
location = response.headers.get("location", "")
assert "/setup" in location or "/settings" in location
@patch("app.utils.settings_service.delete_setting_from_db")
def test_undo_skip_calls_delete(self, mock_delete, client):
"""Test that undo-skip calls delete_setting_from_db."""
mock_delete.return_value = True
# Just ensure the route exists and does not 404
response = client.get("/setup/undo-skip", follow_redirects=False)
assert response.status_code != 404
# ---------------------------------------------------------------------------
# TestDropboxSaveSettingsDbPersist
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestDropboxSaveSettingsDbPersist:
"""Unit tests for save_dropbox_settings DB persistence."""
@patch("app.api.dropbox.settings")
@patch("app.api.dropbox.notify_settings_updated")
@patch("app.api.dropbox.save_setting_to_db")
def test_db_written_even_when_env_missing(self, mock_save, mock_notify, mock_settings, client):
"""Test that DB is written even when .env doesn't exist (no exception)."""
mock_save.return_value = True
with patch("os.path.exists", return_value=False):
response = client.post(
"/api/dropbox/save-settings",
data={"refresh_token": "test-refresh-token"},
follow_redirects=False,
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
mock_save.assert_called()
@patch("app.api.dropbox.settings")
@patch("app.api.dropbox.notify_settings_updated")
@patch("app.api.dropbox.save_setting_to_db")
def test_notify_settings_updated_called(self, mock_save, mock_notify, mock_settings, client):
"""Test that notify_settings_updated is called."""
mock_save.return_value = True
with patch("os.path.exists", return_value=False):
client.post(
"/api/dropbox/save-settings",
data={"refresh_token": "test-refresh-token"},
follow_redirects=False,
)
mock_notify.assert_called_once()
@patch("app.api.dropbox.settings")
@patch("app.api.dropbox.notify_settings_updated")
@patch("app.api.dropbox.save_setting_to_db")
def test_all_provided_values_persisted(self, mock_save, mock_notify, mock_settings, client):
"""Test that all provided values are persisted to DB."""
mock_save.return_value = True
with patch("os.path.exists", return_value=False):
client.post(
"/api/dropbox/save-settings",
data={
"refresh_token": "tok",
"app_key": "key",
"app_secret": "secret",
"folder_path": "/uploads",
},
follow_redirects=False,
)
keys_saved = [call[0][1] for call in mock_save.call_args_list]
assert "dropbox_refresh_token" in keys_saved
assert "dropbox_app_key" in keys_saved
assert "dropbox_app_secret" in keys_saved
assert "dropbox_folder" in keys_saved
# ---------------------------------------------------------------------------
# TestGoogleDriveUpdateSettingsDbPersist
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestGoogleDriveUpdateSettingsDbPersist:
"""Unit tests for update_google_drive_settings DB persistence."""
@patch("app.api.google_drive.settings")
@patch("app.api.google_drive.notify_settings_updated")
@patch("app.api.google_drive.save_setting_to_db")
def test_db_written_for_each_provided_field(self, mock_save, mock_notify, mock_settings, client):
"""Test that DB is written for each provided field."""
mock_save.return_value = True
response = client.post(
"/api/google-drive/update-settings",
data={
"refresh_token": "gdrive-refresh",
"client_id": "client-id",
"client_secret": "client-secret",
"folder_id": "folder-123",
"use_oauth": "true",
},
follow_redirects=False,
)
assert response.status_code == 200
keys_saved = [call[0][1] for call in mock_save.call_args_list]
assert "google_drive_refresh_token" in keys_saved
assert "google_drive_client_id" in keys_saved
assert "google_drive_client_secret" in keys_saved
assert "google_drive_folder_id" in keys_saved
assert "google_drive_use_oauth" in keys_saved
@patch("app.api.google_drive.settings")
@patch("app.api.google_drive.notify_settings_updated")
@patch("app.api.google_drive.save_setting_to_db")
def test_use_oauth_saved_as_lowercase_string(self, mock_save, mock_notify, mock_settings, client):
"""Test that use_oauth is saved as 'true' or 'false' string."""
mock_save.return_value = True
client.post(
"/api/google-drive/update-settings",
data={"refresh_token": "tok", "use_oauth": "true"},
follow_redirects=False,
)
use_oauth_calls = [call for call in mock_save.call_args_list if call[0][1] == "google_drive_use_oauth"]
assert len(use_oauth_calls) == 1
assert use_oauth_calls[0][0][2] in ("true", "false")
@patch("app.api.google_drive.settings")
@patch("app.api.google_drive.notify_settings_updated")
@patch("app.api.google_drive.save_setting_to_db")
def test_notify_called(self, mock_save, mock_notify, mock_settings, client):
"""Test that notify_settings_updated is called."""
mock_save.return_value = True
client.post(
"/api/google-drive/update-settings",
data={"refresh_token": "tok"},
follow_redirects=False,
)
mock_notify.assert_called_once()
# ---------------------------------------------------------------------------
# TestOneDriveSaveSettingsDbPersist
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestOneDriveSaveSettingsDbPersist:
"""Unit tests for save_onedrive_settings DB persistence."""
@patch("app.api.onedrive.settings")
@patch("app.api.onedrive.notify_settings_updated")
@patch("app.api.onedrive.save_setting_to_db")
def test_db_written_even_without_env_file(self, mock_save, mock_notify, mock_settings, client):
"""Test that DB is written even when .env file does not exist."""
mock_save.return_value = True
with patch("os.path.exists", return_value=False):
response = client.post(
"/api/onedrive/save-settings",
data={"refresh_token": "od-refresh", "tenant_id": "common"},
follow_redirects=False,
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
mock_save.assert_called()
@patch("app.api.onedrive.settings")
@patch("app.api.onedrive.notify_settings_updated")
@patch("app.api.onedrive.save_setting_to_db")
def test_all_fields_persisted(self, mock_save, mock_notify, mock_settings, client):
"""Test that all provided fields are persisted to DB."""
mock_save.return_value = True
with patch("os.path.exists", return_value=False):
client.post(
"/api/onedrive/save-settings",
data={
"refresh_token": "tok",
"client_id": "cid",
"client_secret": "csec",
"tenant_id": "my-tenant",
"folder_path": "/docs",
},
follow_redirects=False,
)
keys_saved = [call[0][1] for call in mock_save.call_args_list]
assert "onedrive_refresh_token" in keys_saved
assert "onedrive_client_id" in keys_saved
assert "onedrive_client_secret" in keys_saved
assert "onedrive_tenant_id" in keys_saved
assert "onedrive_folder_path" in keys_saved
@patch("app.api.onedrive.settings")
@patch("app.api.onedrive.notify_settings_updated")
@patch("app.api.onedrive.save_setting_to_db")
def test_notify_called(self, mock_save, mock_notify, mock_settings, client):
"""Test that notify_settings_updated is called."""
mock_save.return_value = True
with patch("os.path.exists", return_value=False):
client.post(
"/api/onedrive/save-settings",
data={"refresh_token": "tok"},
follow_redirects=False,
)
mock_notify.assert_called_once()
# ---------------------------------------------------------------------------
# TestGetSettingsForExport
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestGetSettingsForExport:
"""Unit tests for the get_settings_for_export service function."""
def test_source_db_returns_only_db_settings(self, db_session):
"""Test that source=db returns only DB-persisted settings."""
from app.utils.settings_service import get_settings_for_export
save_setting_to_db(db_session, "workdir", "/tmp/test", changed_by="test")
result = get_settings_for_export(db_session, source="db")
assert "WORKDIR" in result
assert result["WORKDIR"] == "/tmp/test"
def test_source_effective_includes_metadata_keys(self, db_session):
"""Test that source=effective includes keys from SETTING_METADATA."""
from app.utils.settings_service import get_settings_for_export
result = get_settings_for_export(db_session, source="effective")
# The effective export should include keys from SETTING_METADATA that have values
# At a minimum check it returns a dict
assert isinstance(result, dict)
# Keys should be uppercase
for k in result:
assert k == k.upper()
def test_keys_are_uppercase(self, db_session):
"""Test that all keys are returned in uppercase."""
from app.utils.settings_service import get_settings_for_export
save_setting_to_db(db_session, "workdir", "/tmp", changed_by="test")
result = get_settings_for_export(db_session, source="db")
for k in result:
assert k == k.upper(), f"Key {k!r} is not uppercase"
def test_none_values_excluded(self, db_session):
"""Test that None values are excluded from the export."""
from app.utils.settings_service import get_settings_for_export
result = get_settings_for_export(db_session, source="db")
for v in result.values():
assert v is not None
def test_db_only_excludes_env_only_values(self, db_session):
"""Test that source=db does NOT include ENV-only values (only DB rows)."""
from app.utils.settings_service import get_settings_for_export
# Ensure no settings in DB
result = get_settings_for_export(db_session, source="db")
# DB is empty so result should be empty
assert len(result) == 0
# ---------------------------------------------------------------------------
# TestExportEnvEndpoint
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestExportEnvEndpoint:
"""Unit tests for export_env_settings endpoint function."""
def test_requires_admin(self, client):
"""Test that the endpoint requires admin access (no session)."""
response = client.get("/api/settings/export-env")
assert response.status_code in (302, 401, 403)
def test_returns_text_plain(self, db_session):
"""Test that the endpoint returns text/plain response."""
import asyncio
from app.api.settings import export_env_settings
mock_request = MagicMock()
mock_admin = {"id": "admin", "is_admin": True}
result = asyncio.run(export_env_settings(mock_request, db_session, mock_admin, source="db"))
assert result.media_type == "text/plain"
def test_content_disposition_header(self, db_session):
"""Test that the response includes a content-disposition attachment header."""
import asyncio
from app.api.settings import export_env_settings
mock_request = MagicMock()
mock_admin = {"id": "admin", "is_admin": True}
result = asyncio.run(export_env_settings(mock_request, db_session, mock_admin, source="db"))
cd = result.headers.get("content-disposition", "")
assert "attachment" in cd
assert ".env" in cd
def test_invalid_source_returns_400(self, db_session):
"""Test that an invalid source parameter raises HTTPException 400."""
import asyncio
from fastapi import HTTPException
from app.api.settings import export_env_settings
mock_request = MagicMock()
mock_admin = {"id": "admin", "is_admin": True}
with pytest.raises(HTTPException) as exc_info:
asyncio.run(export_env_settings(mock_request, db_session, mock_admin, source="invalid"))
assert exc_info.value.status_code == 400
def test_default_source_is_db(self, db_session):
"""Test that default source is db (filename contains 'db')."""
import asyncio
from app.api.settings import export_env_settings
mock_request = MagicMock()
mock_admin = {"id": "admin", "is_admin": True}
result = asyncio.run(export_env_settings(mock_request, db_session, mock_admin))
cd = result.headers.get("content-disposition", "")
assert "db" in cd
def test_effective_source_returns_response(self, db_session):
"""Test that source=effective returns a valid response."""
import asyncio
from app.api.settings import export_env_settings
mock_request = MagicMock()
mock_admin = {"id": "admin", "is_admin": True}
result = asyncio.run(export_env_settings(mock_request, db_session, mock_admin, source="effective"))
assert result.media_type == "text/plain"
def test_output_contains_docuelevate_header(self, db_session):
"""Test that the export output contains a DocuElevate header comment."""
import asyncio
from app.api.settings import export_env_settings
mock_request = MagicMock()
mock_admin = {"id": "admin", "is_admin": True}
result = asyncio.run(export_env_settings(mock_request, db_session, mock_admin, source="db"))
assert b"DocuElevate" in result.body