feat: comprehensive test infrastructure improvements
- Fix unit test imports: add env setup in conftest.py before module imports - Add 24 new auth router tests (RBAC, preferences, password validation) - Add 16 new tests for litellm and chat_summary routers - Apply black formatting and ruff linting across codebase - Add pre-commit hooks configuration (black, ruff, file checks) - Increase CI coverage threshold from 40% to 50% Test Results: - 206 tests passing (91 unit + 115 integration) - Coverage: 58.79% on core modules - auth.py: 57% → 85%, litellm.py: 23% → 87%, chat_summary.py: 41% → 100% - auth_service: 96.51%, config: 100%, rbac: 93.48%
This commit is contained in:
@@ -1,12 +1,23 @@
|
||||
"""
|
||||
Shared pytest fixtures for backend tests.
|
||||
"""
|
||||
import os
|
||||
|
||||
import json
|
||||
import pytest
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
|
||||
# Set up environment variables before any imports during pytest collection
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-at-least-32-chars-long")
|
||||
os.environ.setdefault("ADMIN_USER", "testadmin")
|
||||
os.environ.setdefault("ADMIN_PASSWORD_HASH", "$pbkdf2-sha256$29000$N2YMIQQgBAAgxBgjhPD.vw$1234567890abcdef")
|
||||
os.environ.setdefault("VOLUME_ROOT", "/tmp/test-volume")
|
||||
os.environ.setdefault("DOCKER_HOST", "tcp://mock-docker:2375")
|
||||
os.environ.setdefault("GITEA_URL", "http://mock-gitea:3000")
|
||||
os.environ.setdefault("GITEA_TOKEN", "mock-token")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -30,8 +41,10 @@ def mock_config(temp_volume_root, monkeypatch):
|
||||
monkeypatch.setenv("GITEA_TOKEN", "mock-token")
|
||||
|
||||
# Reload config module to pick up new env vars
|
||||
import config
|
||||
import importlib
|
||||
|
||||
import config
|
||||
|
||||
importlib.reload(config)
|
||||
|
||||
return config
|
||||
@@ -41,12 +54,7 @@ def mock_config(temp_volume_root, monkeypatch):
|
||||
def temp_auth_file(temp_volume_root):
|
||||
"""Create temporary auth.json file."""
|
||||
auth_file = os.path.join(temp_volume_root, "docker/nas-dashboard/auth.json")
|
||||
data = {
|
||||
"totp_secret": "",
|
||||
"passkey_credentials": [],
|
||||
"token_version": 0,
|
||||
"last_totp_ts": 0
|
||||
}
|
||||
data = {"totp_secret": "", "passkey_credentials": [], "token_version": 0, "last_totp_ts": 0}
|
||||
with open(auth_file, "w") as f:
|
||||
json.dump(data, f)
|
||||
return auth_file
|
||||
@@ -60,9 +68,9 @@ def temp_rbac_file(temp_volume_root):
|
||||
"role_defaults": {
|
||||
"admin": {"pages": "*", "sidebar_links": "*"},
|
||||
"member": {"pages": ["dashboard", "docker"], "sidebar_links": "*"},
|
||||
"viewer": {"pages": ["dashboard"], "sidebar_links": ["dashboard"]}
|
||||
"viewer": {"pages": ["dashboard"], "sidebar_links": ["dashboard"]},
|
||||
},
|
||||
"user_overrides": {}
|
||||
"user_overrides": {},
|
||||
}
|
||||
with open(rbac_file, "w") as f:
|
||||
json.dump(data, f)
|
||||
@@ -73,16 +81,15 @@ def temp_rbac_file(temp_volume_root):
|
||||
def valid_access_token(mock_config):
|
||||
"""Generate a valid access token for testing."""
|
||||
from auth_service import create_access_token
|
||||
return create_access_token(
|
||||
data={"sub": "testuser", "role": "admin"},
|
||||
expires_delta=timedelta(minutes=30)
|
||||
)
|
||||
|
||||
return create_access_token(data={"sub": "testuser", "role": "admin"}, expires_delta=timedelta(minutes=30))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def valid_refresh_token(mock_config, temp_auth_file):
|
||||
"""Generate a valid refresh token for testing."""
|
||||
from auth_service import create_refresh_token
|
||||
|
||||
return create_refresh_token(data={"sub": "testuser", "role": "admin"})
|
||||
|
||||
|
||||
@@ -90,10 +97,8 @@ def valid_refresh_token(mock_config, temp_auth_file):
|
||||
def expired_access_token(mock_config):
|
||||
"""Generate an expired access token for testing."""
|
||||
from auth_service import create_access_token
|
||||
return create_access_token(
|
||||
data={"sub": "testuser", "role": "admin"},
|
||||
expires_delta=timedelta(seconds=-1)
|
||||
)
|
||||
|
||||
return create_access_token(data={"sub": "testuser", "role": "admin"}, expires_delta=timedelta(seconds=-1))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -118,10 +123,7 @@ def mock_docker_client():
|
||||
container.status = "running"
|
||||
container.image = mock_image
|
||||
container.ports = {"80/tcp": [{"HostPort": "8080"}]}
|
||||
container.attrs = {
|
||||
"State": {"Health": {"Status": "healthy"}},
|
||||
"Config": {"Image": "test-image:latest"}
|
||||
}
|
||||
container.attrs = {"State": {"Health": {"Status": "healthy"}}, "Config": {"Image": "test-image:latest"}}
|
||||
container.logs.return_value = b"test log line 1\ntest log line 2\n"
|
||||
|
||||
client.containers.list.return_value = [container]
|
||||
@@ -170,8 +172,13 @@ def mock_docker_client_factory():
|
||||
image_tag="my-image:v1.0"
|
||||
)
|
||||
"""
|
||||
def _create_mock(container_name="test-container", container_status="running",
|
||||
image_tag="test-image:latest", container_id="abc123"):
|
||||
|
||||
def _create_mock(
|
||||
container_name="test-container",
|
||||
container_status="running",
|
||||
image_tag="test-image:latest",
|
||||
container_id="abc123",
|
||||
):
|
||||
client = Mock()
|
||||
|
||||
# Mock container image
|
||||
@@ -187,10 +194,7 @@ def mock_docker_client_factory():
|
||||
container.status = container_status
|
||||
container.image = mock_image
|
||||
container.ports = {"80/tcp": [{"HostPort": "8080"}]}
|
||||
container.attrs = {
|
||||
"State": {"Health": {"Status": "healthy"}},
|
||||
"Config": {"Image": image_tag}
|
||||
}
|
||||
container.attrs = {"State": {"Health": {"Status": "healthy"}}, "Config": {"Image": image_tag}}
|
||||
container.logs.return_value = b"test log line 1\ntest log line 2\n"
|
||||
|
||||
client.containers.list.return_value = [container]
|
||||
@@ -229,8 +233,10 @@ def mock_httpx_client():
|
||||
def test_app(mock_config, temp_auth_file, temp_rbac_file, mock_docker_client):
|
||||
"""Create FastAPI test client with mocked dependencies."""
|
||||
# Reload routers to pick up new config values
|
||||
import routers.files
|
||||
import importlib
|
||||
|
||||
import routers.files
|
||||
|
||||
importlib.reload(routers.files)
|
||||
|
||||
# Patch Docker client before importing main
|
||||
@@ -241,6 +247,7 @@ def test_app(mock_config, temp_auth_file, temp_rbac_file, mock_docker_client):
|
||||
|
||||
with patch("slowapi.Limiter", return_value=mock_limiter):
|
||||
from main import app
|
||||
|
||||
client = TestClient(app)
|
||||
yield client
|
||||
|
||||
@@ -258,14 +265,11 @@ def mock_rbac_data():
|
||||
"role_defaults": {
|
||||
"admin": {"pages": "*", "sidebar_links": "*"},
|
||||
"member": {"pages": ["dashboard", "docker", "files"], "sidebar_links": "*"},
|
||||
"viewer": {"pages": ["dashboard"], "sidebar_links": ["dashboard"]}
|
||||
"viewer": {"pages": ["dashboard"], "sidebar_links": ["dashboard"]},
|
||||
},
|
||||
"user_overrides": {
|
||||
"customuser": {
|
||||
"pages": ["dashboard", "docker"],
|
||||
"sidebar_links": ["dashboard", "docker", "files"]
|
||||
}
|
||||
}
|
||||
"customuser": {"pages": ["dashboard", "docker"], "sidebar_links": ["dashboard", "docker", "files"]}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user