Files
gh-christianlouis-docuelevate/tests/test_db_wizard_api.py
T
copilot-swe-agent[bot] 174e4890dd feat(database): integrate wizard into settings page, improve accessibility and test coverage
- Add "DB Wizard" link button to settings page header
- Add help_link to database_url SETTING_METADATA pointing to /database-wizard
- Add help_link rendering in settings template for any setting with a help_link
- Fix SQLite whitespace path handling in build_connection_string
- Add dark mode CSS overrides for wizard template
- Add aria-describedby for all form inputs with help text
- Add prefers-reduced-motion media query for smooth scrolling
- Expand test coverage: 106 tests (up from 49)
  - db_wizard.py: 100% coverage
  - db_wizard view: 100% coverage
  - database.py API: 97.37% coverage
  - db_migrate.py: 96.60% coverage

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-06 10:02:04 +00:00

378 lines
16 KiB
Python

"""Tests for app/api/database.py and app/views/db_wizard.py modules."""
from unittest.mock import patch
import pytest
@pytest.mark.integration
class TestDatabaseApiEndpoints:
"""Tests for the database API endpoints."""
def test_list_backends(self, client):
"""Test GET /api/database/backends returns supported backends."""
response = client.get("/api/database/backends")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) >= 3
ids = [b["id"] for b in data]
assert "sqlite" in ids
assert "postgresql" in ids
def test_build_url_requires_admin(self, client):
"""Test POST /api/database/build-url requires admin."""
response = client.post(
"/api/database/build-url",
json={"backend": "sqlite"},
)
assert response.status_code == 403
def test_build_url_sqlite(self, client):
"""Test building a SQLite URL as admin."""
# Simulate admin session
with client.session_transaction() if hasattr(client, "session_transaction") else _NoOpContextManager():
pass
# Use the session cookie approach
client.cookies.set("session", "test")
with patch("app.api.database._require_admin", return_value={"is_admin": True}):
response = client.post(
"/api/database/build-url",
json={"backend": "sqlite", "sqlite_path": "/data/test.db"},
)
assert response.status_code == 200
assert "sqlite:////data/test.db" in response.json().get("url", "")
def test_build_url_missing_host(self, client):
"""Test building URL with missing host returns 400."""
with patch("app.api.database._require_admin", return_value={"is_admin": True}):
response = client.post(
"/api/database/build-url",
json={"backend": "postgresql", "database": "db", "username": "u"},
)
assert response.status_code == 400
def test_test_connection_sqlite(self, client):
"""Test connection to in-memory SQLite."""
with patch("app.api.database._require_admin", return_value={"is_admin": True}):
response = client.post(
"/api/database/test-connection",
json={"url": "sqlite:///:memory:"},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
def test_validate_url_valid(self, client):
"""Test validate-url with valid SQLite URL."""
with patch("app.api.database._require_admin", return_value={"is_admin": True}):
response = client.post(
"/api/database/validate-url",
json={"url": "sqlite:///test.db"},
)
assert response.status_code == 200
assert response.json()["valid"] is True
def test_validate_url_invalid(self, client):
"""Test validate-url with unsupported backend."""
with patch("app.api.database._require_admin", return_value={"is_admin": True}):
response = client.post(
"/api/database/validate-url",
json={"url": "mssql://u:p@h/d"},
)
assert response.status_code == 200
assert response.json()["valid"] is False
def test_parse_url(self, client):
"""Test parse-url endpoint."""
with patch("app.api.database._require_admin", return_value={"is_admin": True}):
response = client.post(
"/api/database/parse-url",
json={"url": "postgresql://user:pass@host:5432/db"},
)
assert response.status_code == 200
data = response.json()
assert data["backend"] == "postgresql"
assert data["host"] == "host"
def test_preview_migration(self, client):
"""Test preview-migration endpoint."""
with patch("app.api.database._require_admin", return_value={"is_admin": True}):
response = client.post(
"/api/database/preview-migration",
json={"url": "sqlite:///:memory:"},
)
assert response.status_code == 200
data = response.json()
assert "tables" in data
def test_migrate_invalid_source(self, client):
"""Test migrate endpoint with invalid source URL."""
with patch("app.api.database._require_admin", return_value={"is_admin": True}):
response = client.post(
"/api/database/migrate",
json={"source_url": "mssql://bad", "target_url": "sqlite:///:memory:"},
)
assert response.status_code == 400
def test_migrate_invalid_target(self, client):
"""Test migrate endpoint with invalid target URL."""
with patch("app.api.database._require_admin", return_value={"is_admin": True}):
response = client.post(
"/api/database/migrate",
json={"source_url": "sqlite:///:memory:", "target_url": "mssql://bad"},
)
assert response.status_code == 400
def test_migrate_success(self, client):
"""Test migrate endpoint with successful migration."""
mock_result = {"success": True, "tables_copied": 5, "rows_copied": 100, "errors": []}
with patch("app.api.database._require_admin", return_value={"is_admin": True}):
with patch("app.api.database.migrate_data", return_value=mock_result):
response = client.post(
"/api/database/migrate",
json={
"source_url": "sqlite:///:memory:",
"target_url": "sqlite:///:memory:",
},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["rows_copied"] == 100
def test_migrate_failure_returns_500(self, client):
"""Test migrate endpoint returns 500 on migration failure."""
mock_result = {
"success": False,
"tables_copied": 2,
"rows_copied": 50,
"errors": ["Table X failed", "Stamp failed"],
}
with patch("app.api.database._require_admin", return_value={"is_admin": True}):
with patch("app.api.database.migrate_data", return_value=mock_result):
response = client.post(
"/api/database/migrate",
json={
"source_url": "sqlite:///:memory:",
"target_url": "sqlite:///:memory:",
},
)
assert response.status_code == 500
assert "Table X failed" in response.json()["detail"]
def test_test_connection_requires_admin(self, client):
"""Test POST /api/database/test-connection requires admin."""
response = client.post(
"/api/database/test-connection",
json={"url": "sqlite:///:memory:"},
)
assert response.status_code == 403
def test_parse_url_requires_admin(self, client):
"""Test POST /api/database/parse-url requires admin."""
response = client.post(
"/api/database/parse-url",
json={"url": "sqlite:///:memory:"},
)
assert response.status_code == 403
def test_validate_url_requires_admin(self, client):
"""Test POST /api/database/validate-url requires admin."""
response = client.post(
"/api/database/validate-url",
json={"url": "sqlite:///:memory:"},
)
assert response.status_code == 403
def test_preview_migration_requires_admin(self, client):
"""Test POST /api/database/preview-migration requires admin."""
response = client.post(
"/api/database/preview-migration",
json={"url": "sqlite:///:memory:"},
)
assert response.status_code == 403
def test_migrate_requires_admin(self, client):
"""Test POST /api/database/migrate requires admin."""
response = client.post(
"/api/database/migrate",
json={"source_url": "sqlite:///:memory:", "target_url": "sqlite:///:memory:"},
)
assert response.status_code == 403
def test_build_url_postgresql(self, client):
"""Test building a PostgreSQL URL."""
with patch("app.api.database._require_admin", return_value={"is_admin": True}):
response = client.post(
"/api/database/build-url",
json={
"backend": "postgresql",
"host": "localhost",
"port": 5432,
"database": "mydb",
"username": "admin",
"password": "secret",
},
)
assert response.status_code == 200
url = response.json()["url"]
assert "postgresql://admin:secret@localhost:5432/mydb" in url
@pytest.mark.integration
class TestDatabaseWizardView:
"""Tests for the database wizard view."""
def test_database_wizard_page_loads(self, client):
"""Test GET /database-wizard returns 200."""
response = client.get("/database-wizard")
assert response.status_code == 200
def test_database_wizard_contains_title(self, client):
"""Test that the wizard page contains expected content."""
response = client.get("/database-wizard")
assert response.status_code == 200
assert "Database Configuration Wizard" in response.text
def test_database_wizard_contains_tabs(self, client):
"""Test that the wizard page contains configure and migrate tabs."""
response = client.get("/database-wizard")
assert "Configure Database" in response.text
assert "Migrate Data" in response.text
def test_database_wizard_has_skip_link(self, client):
"""Test that the wizard page includes a skip-to-content link."""
response = client.get("/database-wizard")
assert "Skip to main content" in response.text
def test_database_wizard_has_main_landmark(self, client):
"""Test that the wizard page has a main landmark."""
response = client.get("/database-wizard")
assert 'id="main-content"' in response.text
def test_database_wizard_has_tablist_role(self, client):
"""Test that the tab navigation has proper ARIA tablist role."""
response = client.get("/database-wizard")
assert 'role="tablist"' in response.text
assert 'role="tab"' in response.text
assert 'role="tabpanel"' in response.text
def test_database_wizard_has_aria_labels_on_backend_buttons(self, client):
"""Test that backend selection buttons have aria-label attributes."""
response = client.get("/database-wizard")
assert 'aria-label="Select SQLite"' in response.text
assert 'aria-label="Select PostgreSQL"' in response.text
assert 'aria-label="Select MySQL / MariaDB"' in response.text
def test_database_wizard_has_form_labels(self, client):
"""Test that form inputs have associated labels."""
response = client.get("/database-wizard")
assert 'for="sqlite_path"' in response.text
assert 'for="db_host"' in response.text
assert 'for="db_port"' in response.text
assert 'for="db_name"' in response.text
assert 'for="db_user"' in response.text
assert 'for="db_pass"' in response.text
assert 'for="ssl_mode"' in response.text
def test_database_wizard_has_aria_describedby(self, client):
"""Test that inputs have aria-describedby pointing to help text."""
response = client.get("/database-wizard")
assert 'aria-describedby="sqlite_path_help"' in response.text
assert 'id="sqlite_path_help"' in response.text
assert 'aria-describedby="ssl_mode_help"' in response.text
assert 'id="ssl_mode_help"' in response.text
assert 'aria-describedby="mig_source_help"' in response.text
assert 'aria-describedby="mig_target_help"' in response.text
def test_database_wizard_has_status_roles(self, client):
"""Test that dynamic feedback areas have role=status or role=alert."""
response = client.get("/database-wizard")
assert 'role="status"' in response.text
assert 'role="alert"' in response.text
def test_database_wizard_has_aria_live(self, client):
"""Test that dynamic areas have aria-live for screen reader announcements."""
response = client.get("/database-wizard")
assert 'aria-live="polite"' in response.text
def test_database_wizard_has_progressbar(self, client):
"""Test that the migration progress indicator has role=progressbar."""
response = client.get("/database-wizard")
assert 'role="progressbar"' in response.text
def test_database_wizard_has_focus_ring_styles(self, client):
"""Test that interactive elements have focus ring styling."""
response = client.get("/database-wizard")
assert "focus:ring-2" in response.text
assert "focus:outline-none" in response.text
def test_database_wizard_has_table_scope_headers(self, client):
"""Test that migration preview table has proper scope attributes."""
response = client.get("/database-wizard")
assert 'scope="col"' in response.text
def test_database_wizard_has_dark_mode_styles(self, client):
"""Test that the wizard includes dark mode CSS overrides."""
response = client.get("/database-wizard")
assert "html.dark" in response.text
def test_database_wizard_copy_button_has_aria_label(self, client):
"""Test that the copy-to-clipboard button has an aria-label."""
response = client.get("/database-wizard")
assert 'aria-label="Copy to clipboard"' in response.text
def test_database_wizard_decorative_icons_hidden(self, client):
"""Test that decorative icons have aria-hidden=true."""
response = client.get("/database-wizard")
assert 'aria-hidden="true"' in response.text
def test_database_wizard_reduced_motion(self, client):
"""Test that wizard respects prefers-reduced-motion media query."""
response = client.get("/database-wizard")
assert "prefers-reduced-motion" in response.text
@pytest.mark.unit
class TestSettingsPageWizardLink:
"""Tests for the database wizard link on the settings page."""
def test_settings_template_has_db_wizard_link(self):
"""Test that the settings template contains a link to the database wizard."""
from pathlib import Path
template_path = Path(__file__).resolve().parent.parent / "frontend" / "templates" / "settings.html"
content = template_path.read_text()
assert "/database-wizard" in content
assert "DB Wizard" in content
def test_settings_template_has_help_link_rendering(self):
"""Test that the settings template renders help_link metadata."""
from pathlib import Path
template_path = Path(__file__).resolve().parent.parent / "frontend" / "templates" / "settings.html"
content = template_path.read_text()
assert "setting.metadata.get('help_link')" in content
assert "help_link_label" in content
def test_database_url_metadata_has_help_link(self):
"""Test that database_url SETTING_METADATA includes help_link to wizard."""
from app.utils.settings_service import SETTING_METADATA
meta = SETTING_METADATA["database_url"]
assert "help_link" in meta
assert meta["help_link"] == "/database-wizard"
assert "help_link_label" in meta
# Context manager helper for tests that don't need session_transaction
class _NoOpContextManager:
"""Dummy context manager for tests that don't need session_transaction."""
def __enter__(self):
return None
def __exit__(self, *args):
pass