refactor: Make service dependencies explicit and injectable

Remove hidden cross-service coupling by making dependencies explicit through
dependency injection while maintaining backward compatibility via lazy imports.

Key changes:
- history_service and ban_service: Removed direct module-level imports of
  fail2ban_metadata_service, added optional service parameters to functions
- Added get_fail2ban_metadata_service() provider to dependencies.py
- Updated history router to inject Fail2BanMetadataService dependency
- history_service functions now use lazy imports in fallback paths for
  backward compatibility when service is not explicitly injected
- All test patches updated to use internal _get_fail2ban_db_path() helper
- jail_config_service and jail_service already follow best practices

This pattern prevents circular imports, makes services testable via explicit
mocking, and documents service dependencies clearly.

Fixes: Instructions.md #2 - Hidden cross-service coupling

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-27 18:26:08 +02:00
parent bc315b936b
commit 3bbf413c55
12 changed files with 342 additions and 100 deletions

View File

@@ -11,6 +11,7 @@ from app.dependencies import (
Fail2BanConfigDirDep,
Fail2BanSocketDep,
Fail2BanStartCommandDep,
HealthProbeDep,
PendingRecoveryDep,
)
from app.models.config import (
@@ -277,6 +278,7 @@ async def activate_jail(
_auth: AuthDep,
config_dir: Fail2BanConfigDirDep,
socket_path: Fail2BanSocketDep,
health_probe: HealthProbeDep,
name: _NamePath,
body: ActivateJailRequest | None = None,
) -> JailActivationResponse:
@@ -289,6 +291,9 @@ async def activate_jail(
Args:
app: FastAPI application instance.
_auth: Validated session.
config_dir: Absolute path to the fail2ban configuration directory.
socket_path: Path to the fail2ban Unix domain socket.
health_probe: Injectable health probe function for checking fail2ban status.
name: Name of the jail to activate.
body: Optional override values (bantime, findtime, maxretry, port,
logpath).
@@ -304,7 +309,9 @@ async def activate_jail(
"""
req = body if body is not None else ActivateJailRequest()
result = await jail_config_service.activate_jail(config_dir, socket_path, name, req)
result = await jail_config_service.activate_jail(
config_dir, socket_path, name, req, health_probe=health_probe
)
if result.active:
record_activation(app, name)
@@ -323,6 +330,7 @@ async def deactivate_jail(
_auth: AuthDep,
config_dir: Fail2BanConfigDirDep,
socket_path: Fail2BanSocketDep,
health_probe: HealthProbeDep,
name: _NamePath,
) -> JailActivationResponse:
"""Disable an active jail and reload fail2ban.
@@ -332,6 +340,9 @@ async def deactivate_jail(
Args:
_auth: Validated session.
config_dir: Absolute path to the fail2ban configuration directory.
socket_path: Path to the fail2ban Unix domain socket.
health_probe: Injectable health probe function for checking fail2ban status.
name: Name of the jail to deactivate.
Returns:
@@ -344,7 +355,9 @@ async def deactivate_jail(
HTTPException: 502 if fail2ban is unreachable.
"""
result = await jail_config_service.deactivate_jail(config_dir, socket_path, name)
result = await jail_config_service.deactivate_jail(
config_dir, socket_path, name, health_probe=health_probe
)
return result