Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import TYPE_CHECKING

from fastapi_startkit.exceptions.exceptions import DriverNotFound

if TYPE_CHECKING:
from fastapi_startkit.masoniteorm.connections.factory import ConnectionFactory

Expand All @@ -9,6 +11,45 @@ def __init__(self, factory: "ConnectionFactory", config: dict):
self.factory = factory
self.config = config
self.connections = {}
self._validate_config()

def _validate_config(self) -> None:
"""Validate every configured connection up front so a bad config fails
loudly at app-boot/provider-registration time, instead of lazily on
first use (e.g. db:migrate or the first query)."""
from fastapi_startkit.masoniteorm.connections.factory import ConnectionFactory

for name, conn_config in self.config.get("connections", {}).items():
if conn_config.get("url"):
# An explicit url bypasses driver/host/port assembly entirely at
# connection time too (see ConnectionFactory.build_url()), so it
# is exempt from the checks below.
continue

driver = conn_config.get("driver")
if not driver:
raise DriverNotFound(
f"Connection '{name}' is missing a required 'driver' key. "
f"Supported drivers are: {', '.join(sorted(ConnectionFactory.DRIVER_URLS))}."
)

if driver not in ConnectionFactory.DRIVER_URLS:
raise DriverNotFound(
f"Connection '{name}' has an unsupported driver {driver!r}. "
f"Supported drivers are: {', '.join(sorted(ConnectionFactory.DRIVER_URLS))}."
)

if driver == "sqlite":
continue

port = conn_config.get("port")
if port not in (None, ""):
try:
int(port)
except (TypeError, ValueError):
raise ValueError(
f"Connection '{name}' has an invalid 'port' value {port!r}; expected a number."
) from None

def connection(self, name: str | None = None):
name = self.get_default_connection_name(name)
Expand Down
99 changes: 99 additions & 0 deletions fastapi_startkit/tests/masoniteorm/config/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path

from fastapi_startkit.application import Application
from fastapi_startkit.exceptions.exceptions import DriverNotFound
from fastapi_startkit.masoniteorm import SQLiteConfig
from fastapi_startkit.masoniteorm.connections.factory import ConnectionFactory
from fastapi_startkit.masoniteorm.connections.manager import DatabaseManager
Expand Down Expand Up @@ -52,6 +53,104 @@ def test_missing_connection_raises_value_error_not_key_error(self):
manager.connection("sqlite")


class TestDatabaseManagerBootTimeValidation(unittest.TestCase):
"""Task #1232: a misconfigured connection must fail loudly at
construction (app-boot/provider-registration) time, not lazily on first
use — building on top of PR #194's DriverNotFound exception.
"""

def test_valid_configs_still_construct_without_error(self):
"""No behavior change for well-formed configs (sqlite/mysql/postgres)."""
config = {
"default": "sqlite",
"connections": {
"sqlite": {"driver": "sqlite", "database": "app.sqlite3"},
"mysql": {"driver": "mysql", "host": "localhost", "database": "db"},
"postgres": {"driver": "postgres", "host": "localhost", "port": 5432, "database": "db"},
},
}

manager = DatabaseManager(ConnectionFactory(), config)

self.assertIsInstance(manager, DatabaseManager)

def test_missing_driver_key_raises_at_construction(self):
config = {
"default": "sqlite",
"connections": {"sqlite": {"database": "app.sqlite3"}},
}

with self.assertRaises(DriverNotFound) as ctx:
DatabaseManager(ConnectionFactory(), config)
self.assertIn("sqlite", str(ctx.exception))

def test_unsupported_driver_raises_at_construction(self):
config = {
"default": "mssql",
"connections": {"mssql": {"driver": "mssql", "host": "localhost", "database": "db"}},
}

with self.assertRaises(DriverNotFound) as ctx:
DatabaseManager(ConnectionFactory(), config)
self.assertIn("mssql", str(ctx.exception))

def test_non_numeric_port_raises_at_construction(self):
config = {
"default": "postgres",
"connections": {
"postgres": {"driver": "postgres", "host": "localhost", "port": "not-a-port", "database": "db"}
},
}

with self.assertRaises(ValueError) as ctx:
DatabaseManager(ConnectionFactory(), config)
self.assertIn("port", str(ctx.exception))

def test_missing_port_is_not_an_error(self):
"""Missing/empty port is not a misconfiguration -- ConnectionFactory
fills in a per-driver default, so it must not fail validation."""
config = {
"default": "mysql",
"connections": {"mysql": {"driver": "mysql", "host": "localhost", "database": "db"}},
}

manager = DatabaseManager(ConnectionFactory(), config)

self.assertIsInstance(manager, DatabaseManager)

def test_explicit_url_bypasses_driver_and_port_checks(self):
"""A connection configured entirely via `url` skips field validation,
mirroring ConnectionFactory.build_url()'s own url passthrough."""
config = {
"default": "oracle",
"connections": {"oracle": {"driver": "oracle", "url": "sqlite+aiosqlite:///:memory:"}},
}

manager = DatabaseManager(ConnectionFactory(), config)

self.assertIsInstance(manager, DatabaseManager)

def test_bad_connection_fails_at_provider_registration_not_first_query(self):
"""End-to-end: DatabaseProvider.register() runs during Application
construction, so a bad config must raise while booting the app --
before `db.connection()` is ever called."""
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaises(DriverNotFound):
Application(
base_path=Path(tmp),
env="testing",
providers=[
(
DatabaseProvider,
{
"default": "mssql",
"connections": {"mssql": {"driver": "mssql", "host": "localhost", "database": "db"}},
},
)
],
)


class TestDatabaseProviderWiring(unittest.TestCase):
"""DatabaseProvider.register() must produce a DatabaseManager whose
connection() resolves cleanly — this is what `python artisan db:migrate`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,19 @@ def test_connection_is_cached(self, db):
assert conn1 is conn2

def test_connection_raises_for_missing_driver(self):
# make() now validates the driver before any engine is built, so an
# unsupported driver fails fast with a friendly framework error rather
# than a raw KeyError/ValueError from deep inside SQLAlchemy.
# DatabaseManager now validates every configured connection's driver at
# construction (app-boot) time, so an unsupported driver fails fast with
# a friendly framework error before any engine is ever built, rather than
# lazily on first use.
factory = ConnectionFactory()
bad_config = {
"default": "mssql",
"connections": {
"mssql": {"driver": "mssql", "host": "localhost", "database": "db"},
},
}
dm = DatabaseManager(factory, bad_config)
with pytest.raises(DriverNotFound, match="mssql"):
dm.connection("mssql")
DatabaseManager(factory, bad_config)


# ---------------------------------------------------------------------------
Expand Down
Loading