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>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-06 10:02:04 +00:00
parent 330c3aedb6
commit 174e4890dd
7 changed files with 629 additions and 12 deletions
+219
View File
@@ -124,6 +124,101 @@ class TestDatabaseApiEndpoints:
)
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:
@@ -146,6 +241,130 @@ class TestDatabaseWizardView:
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: