Files
nas-tools/dashboard/backend/tests/conftest.py
T
Gan, Jimmy 5c7d185953
Deploy Dashboard (Dev) / Deploy to Dev (push) Failing after 1m52s
Run Tests / Backend Tests (pull_request) Failing after 47s
Run Tests / Frontend Tests (pull_request) Failing after 4m27s
Run Tests / Test Summary (pull_request) Failing after 17s
fix: update mock database schema to match conversation tracker queries
- Add missing columns: slug, model, conversation_count, total_messages, total_tokens, created_at, project_path
- Fix test data insert to include all required columns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 16:40:24 +08:00

360 lines
11 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, mock_conversation_db):
"""Create FastAPI test client with mocked dependencies."""
# Reload routers to pick up new config values
import importlib
import routers.files
import routers.conversation_tracker
importlib.reload(routers.files)
importlib.reload(routers.conversation_tracker)
# 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
@pytest.fixture
def mock_conversation_db(tmp_path, monkeypatch):
"""Mock conversation tracker database"""
import sqlite3
db_path = tmp_path / "conversations.db"
# Create minimal SQLite schema
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE conversations (
session_id TEXT PRIMARY KEY,
project_path TEXT,
slug TEXT,
started_at DATETIME,
message_count INTEGER,
total_input_tokens INTEGER DEFAULT 0,
total_output_tokens INTEGER DEFAULT 0,
model TEXT
)
""")
conn.execute("""
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
message_uuid TEXT UNIQUE NOT NULL,
role TEXT NOT NULL,
content_text TEXT,
timestamp DATETIME NOT NULL
)
""")
conn.execute("""
CREATE TABLE tool_calls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_uuid TEXT NOT NULL,
tool_name TEXT NOT NULL,
timestamp DATETIME NOT NULL
)
""")
conn.execute("""
CREATE TABLE daily_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT UNIQUE NOT NULL,
summary_content TEXT NOT NULL,
conversation_count INTEGER DEFAULT 0,
total_messages INTEGER DEFAULT 0,
total_tokens INTEGER DEFAULT 0,
created_at DATETIME,
project_path TEXT
)
""")
# Insert test data
conn.execute("""
INSERT INTO conversations VALUES
('test-session-id', '/test/project', 'test-slug', '2026-04-19 10:00:00', 10, 1000, 500, 'claude-sonnet-4')
""")
conn.execute("""
INSERT INTO messages VALUES
(1, 'test-session-id', 'msg-1', 'user', 'test message', '2026-04-19 10:00:00')
""")
conn.commit()
conn.close()
monkeypatch.setenv("CONVERSATION_TRACKER_DB", str(db_path))
return db_path