Files
nas-tools/dashboard/backend/tests/conftest.py
Gan, Jimmy b981c06d59
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 2m26s
Run Tests / Backend Tests (push) Failing after 2m36s
Run Tests / Frontend Tests (push) Failing after 2m23s
Run Tests / Test Summary (push) Failing after 13s
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%
2026-04-08 00:21:32 +08:00

290 lines
8.9 KiB
Python

"""
Shared pytest fixtures for backend tests.
"""
import json
import os
from datetime import timedelta
from unittest.mock import AsyncMock, Mock, patch
import pytest
from fastapi.testclient import TestClient
# 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
def temp_volume_root(tmp_path):
"""Create temporary volume root for testing."""
volume_root = tmp_path / "volume1"
volume_root.mkdir()
(volume_root / "docker" / "nas-dashboard").mkdir(parents=True)
return str(volume_root)
@pytest.fixture
def mock_config(temp_volume_root, monkeypatch):
"""Mock config module with test values."""
monkeypatch.setenv("SECRET_KEY", "test-secret-key-at-least-32-chars-long")
monkeypatch.setenv("ADMIN_USER", "testadmin")
monkeypatch.setenv("ADMIN_PASSWORD_HASH", "$pbkdf2-sha256$29000$N2YMIQQgBAAgxBgjhPD.vw$1234567890abcdef")
monkeypatch.setenv("VOLUME_ROOT", temp_volume_root)
monkeypatch.setenv("DOCKER_HOST", "tcp://mock-docker:2375")
monkeypatch.setenv("GITEA_URL", "http://mock-gitea:3000")
monkeypatch.setenv("GITEA_TOKEN", "mock-token")
# Reload config module to pick up new env vars
import importlib
import config
importlib.reload(config)
return config
@pytest.fixture
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}
with open(auth_file, "w") as f:
json.dump(data, f)
return auth_file
@pytest.fixture
def temp_rbac_file(temp_volume_root):
"""Create temporary rbac.json file."""
rbac_file = os.path.join(temp_volume_root, "docker/nas-dashboard/rbac.json")
data = {
"role_defaults": {
"admin": {"pages": "*", "sidebar_links": "*"},
"member": {"pages": ["dashboard", "docker"], "sidebar_links": "*"},
"viewer": {"pages": ["dashboard"], "sidebar_links": ["dashboard"]},
},
"user_overrides": {},
}
with open(rbac_file, "w") as f:
json.dump(data, f)
return rbac_file
@pytest.fixture
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))
@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"})
@pytest.fixture
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))
@pytest.fixture
def mock_docker_client():
"""Mock Docker client for testing.
Returns a mock client with a single running container.
For error scenarios, use mock_docker_client_with_errors.
"""
client = Mock()
# Mock container image
mock_image = Mock()
mock_image.tags = ["test-image:latest"]
mock_image.id = "sha256:abc123def456"
# Mock container
container = Mock()
container.id = "abc123"
container.short_id = "abc123"
container.name = "test-container"
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.logs.return_value = b"test log line 1\ntest log line 2\n"
client.containers.list.return_value = [container]
client.containers.get.return_value = container
return client
@pytest.fixture
def mock_docker_client_with_errors():
"""Mock Docker client that simulates error scenarios.
Use this fixture to test error handling:
- Container not found (NotFound exception)
- Connection errors (APIError)
- Permission denied (APIError with 403)
Example:
def test_container_not_found(test_app, admin_headers, mock_docker_client_with_errors):
# Configure the mock to raise NotFound
import docker.errors
mock_docker_client_with_errors.containers.get.side_effect = docker.errors.NotFound("Container not found")
"""
import docker.errors
client = Mock()
# By default, raise NotFound for get operations
client.containers.get.side_effect = docker.errors.NotFound("Container not found")
client.containers.list.return_value = []
return client
@pytest.fixture
def mock_docker_client_factory():
"""Factory for creating customizable Docker client mocks.
Returns a function that creates mock clients with specific configurations.
Example:
def test_custom_container(mock_docker_client_factory):
client = mock_docker_client_factory(
container_name="my-container",
container_status="stopped",
image_tag="my-image:v1.0"
)
"""
def _create_mock(
container_name="test-container",
container_status="running",
image_tag="test-image:latest",
container_id="abc123",
):
client = Mock()
# Mock container image
mock_image = Mock()
mock_image.tags = [image_tag]
mock_image.id = f"sha256:{container_id}def456"
# Mock container
container = Mock()
container.id = container_id
container.short_id = container_id[:12]
container.name = container_name
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.logs.return_value = b"test log line 1\ntest log line 2\n"
client.containers.list.return_value = [container]
client.containers.get.return_value = container
return client
return _create_mock
@pytest.fixture
def mock_ssh_connection():
"""Mock asyncssh connection for testing."""
conn = AsyncMock()
process = AsyncMock()
process.stdout = AsyncMock()
process.stdin = AsyncMock()
process.stderr = AsyncMock()
conn.create_process.return_value = process
return conn
@pytest.fixture
def mock_httpx_client():
"""Mock httpx AsyncClient for testing."""
client = AsyncMock()
response = AsyncMock()
response.status_code = 200
response.json.return_value = {"status": "ok"}
client.get.return_value = response
client.post.return_value = response
return client
@pytest.fixture
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 importlib
import routers.files
importlib.reload(routers.files)
# Patch Docker client before importing main
with patch("docker.DockerClient", return_value=mock_docker_client):
# Mock the limiter to disable rate limiting during tests
mock_limiter = Mock()
mock_limiter.limit = lambda *args, **kwargs: lambda func: func
with patch("slowapi.Limiter", return_value=mock_limiter):
from main import app
client = TestClient(app)
yield client
@pytest.fixture
def admin_headers(valid_access_token):
"""Headers with admin access token."""
return {"Authorization": f"Bearer {valid_access_token}"}
@pytest.fixture
def mock_rbac_data():
"""Sample RBAC configuration for testing."""
return {
"role_defaults": {
"admin": {"pages": "*", "sidebar_links": "*"},
"member": {"pages": ["dashboard", "docker", "files"], "sidebar_links": "*"},
"viewer": {"pages": ["dashboard"], "sidebar_links": ["dashboard"]},
},
"user_overrides": {
"customuser": {"pages": ["dashboard", "docker"], "sidebar_links": ["dashboard", "docker", "files"]}
},
}
@pytest.fixture
def sample_totp_secret():
"""Sample TOTP secret for testing."""
return "JBSWY3DPEHPK3PXP" # Base32 encoded secret
@pytest.fixture
def mock_request():
"""Mock FastAPI Request object."""
request = Mock()
request.client = Mock()
request.client.host = "192.168.31.100"
request.headers = {}
return request