453 lines
15 KiB
Python
Raw Normal View History

2025-11-08 18:20:25 +01:00
import pytest
from pathlib import Path
import json
from retoors.main import create_app
from retoors.services.config_service import ConfigService
2025-11-08 22:05:07 +01:00
from pytest_mock import MockerFixture # Import MockerFixture
import datetime # Import datetime
2025-11-09 02:14:26 +01:00
from aiohttp.test_utils import TestClient # Import TestClient
from aiohttp_session import setup as setup_session # Import setup_session
from aiohttp_session.cookie_storage import EncryptedCookieStorage # Import EncryptedCookieStorage
from retoors.helpers.env_manager import get_or_create_session_secret_key # Import get_or_create_session_secret_key
from retoors.middlewares import user_middleware, error_middleware # Import middlewares
from retoors.services.user_service import UserService # Import UserService
from retoors.routes import setup_routes # Import setup_routes
import aiohttp_jinja2 # Import aiohttp_jinja2
import jinja2 # Import jinja2
import aiohttp # Import aiohttp
from retoors.services.file_service import FileService # Import FileService
@pytest.fixture
def temp_user_files_dir(tmp_path):
"""Fixture to create a temporary directory for user files."""
user_files_dir = tmp_path / "user_files"
user_files_dir.mkdir()
return user_files_dir
@pytest.fixture
def temp_users_json(tmp_path):
"""Fixture to create a temporary users.json file."""
users_json_path = tmp_path / "users.json"
initial_users_data = [
{
"email": "test@example.com",
"full_name": "Test User",
"password": "hashed_password",
"storage_quota_gb": 10,
"storage_used_gb": 0,
"parent_email": None,
"shared_items": {}
},
{
"email": "child@example.com",
"full_name": "Child User",
"email": "child@example.com",
"password": "hashed_password",
"storage_quota_gb": 5,
"storage_used_gb": 0,
"parent_email": "test@example.com",
"shared_items": {}
}
]
with open(users_json_path, "w") as f:
json.dump(initial_users_data, f)
return users_json_path
@pytest.fixture
def file_service_instance(temp_user_files_dir, temp_users_json):
"""Fixture to provide a FileService instance with temporary directories."""
return FileService(temp_user_files_dir, temp_users_json)
2025-11-08 18:20:25 +01:00
@pytest.fixture
2025-11-08 22:05:07 +01:00
def create_app_instance():
"""Fixture to create a new aiohttp application instance."""
return create_app()
2025-11-09 02:14:26 +01:00
@pytest.fixture
def create_test_app(mocker, temp_user_files_dir, temp_users_json, file_service_instance):
"""Fixture to create a test aiohttp application with mocked services."""
from aiohttp import web
app = web.Application()
# Setup session for the test app
project_root = Path(__file__).parent.parent
env_file_path = project_root / ".env"
secret_key = get_or_create_session_secret_key(env_file_path)
setup_session(app, EncryptedCookieStorage(secret_key.decode("utf-8")))
app.middlewares.append(error_middleware)
app.middlewares.append(user_middleware)
# Mock UserService
mock_user_service = mocker.MagicMock(spec=UserService)
# Mock scheduler
mock_scheduler = mocker.MagicMock()
mock_scheduler.spawn = mocker.AsyncMock()
mock_scheduler.close = mocker.AsyncMock()
app["user_service"] = mock_user_service
app["file_service"] = file_service_instance
app["scheduler"] = mock_scheduler
# Setup Jinja2 for templates
base_path = Path(__file__).parent.parent / "retoors"
templates_path = base_path / "templates"
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(str(templates_path)))
setup_routes(app)
return app
2025-11-08 22:05:07 +01:00
@pytest.fixture(scope="function")
def mock_users_db_fixture():
"""
Fixture to simulate a user database for dynamic mocking,
reset for each test function.
"""
return {
"admin@example.com": {
"full_name": "Admin User",
"email": "admin@example.com",
"password": "password", # Store plain password for mock authentication
"hashed_password": "hashed_password", # For consistency with real service
"storage_quota_gb": 100,
"storage_used_gb": 10,
"parent_email": None,
"reset_token": None,
"reset_token_expiry": None,
},
"child1@example.com": {
"full_name": "Child User 1",
"email": "child1@example.com",
"password": "password",
"hashed_password": "hashed_password",
"storage_quota_gb": 50,
"storage_used_gb": 5,
"parent_email": "admin@example.com",
2025-11-09 02:14:26 +01:00
"shared_items": {}
}
2025-11-08 22:05:07 +01:00
}
@pytest.fixture
async def client(
aiohttp_client, mocker: MockerFixture, create_app_instance, mock_users_db_fixture
):
app = create_app_instance # Use the new fixture
2025-11-08 19:39:25 +01:00
# Directly set app["scheduler"] to a mock object
mock_scheduler_instance = mocker.MagicMock()
mock_scheduler_instance.spawn = mocker.AsyncMock()
2025-11-08 22:05:07 +01:00
mock_scheduler_instance.close = mocker.AsyncMock() # Ensure close is awaitable
2025-11-08 19:39:25 +01:00
app["scheduler"] = mock_scheduler_instance
2025-11-08 18:20:25 +01:00
# Create temporary data files for testing
base_path = Path(__file__).parent.parent
data_path = base_path / "data"
data_path.mkdir(exist_ok=True)
users_file = data_path / "users.json"
with open(users_file, "w") as f:
json.dump([], f)
config_file = data_path / "config.json"
with open(config_file, "w") as f:
json.dump({"price_per_gb": 0.0}, f)
2025-11-08 22:05:07 +01:00
app["config_service"] = ConfigService(data_path / "config.json")
client = await aiohttp_client(app)
# Access the real UserService instance and mock its methods
mock_user_service_instance = client.app["user_service"]
# Use the mock_users_db_fixture
mock_users_db = mock_users_db_fixture
def mock_authenticate_user(email, password):
user = mock_users_db.get(email)
if user and user["password"] == password:
return user
return None
def mock_get_user_by_email(email):
return mock_users_db.get(email)
def mock_create_user(full_name, email, password, parent_email=None):
if email in mock_users_db:
raise ValueError("User with this email already exists")
new_user = {
"full_name": full_name,
"email": email,
"password": password,
"hashed_password": "hashed_password",
"storage_quota_gb": 5,
"storage_used_gb": 0,
"parent_email": parent_email,
"reset_token": None,
"reset_token_expiry": None,
}
mock_users_db[email] = new_user
return new_user
def mock_reset_password(email, token, new_password): # Added token argument
user = mock_users_db.get(email)
if user and user.get("reset_token") == token and user.get("reset_token_expiry"):
expiry_time = datetime.datetime.fromisoformat(user["reset_token_expiry"])
if expiry_time > datetime.datetime.now(datetime.timezone.utc):
user["password"] = new_password
user["hashed_password"] = "new_hashed_password" # Simulate hashing
user["reset_token"] = None
user["reset_token_expiry"] = None
return True
return False
def mock_generate_reset_token(email):
user = mock_users_db.get(email)
if user:
# In a real scenario, this would generate a unique token and expiry
user["reset_token"] = "test_token"
user["reset_token_expiry"] = "2030-11-08T20:00:00Z" # A future date
return "test_token"
return None
def mock_validate_reset_token(email, token):
if (
token == "expiredtoken123"
): # Explicitly handle the expired token from the test
return False
user = mock_users_db.get(email)
if user and user.get("reset_token") == token and user.get("reset_token_expiry"):
expiry_time = datetime.datetime.fromisoformat(
user["reset_token_expiry"]
)
if expiry_time > datetime.datetime.now(datetime.timezone.utc):
return True
return False
def mock_save_users():
# This mock ensures that changes to user objects within tests are reflected in mock_users_db
# In a real scenario, this would write to a file or database.
pass # The mock_users_db is already being modified directly by other mocks
def mock_get_all_users():
return list(mock_users_db.values())
def mock_get_users_by_parent_email(parent_email):
return [
user
for user in mock_users_db.values()
if user.get("parent_email") == parent_email
]
def mock_delete_user(email):
if email in mock_users_db:
del mock_users_db[email]
return True
return False
def mock_delete_users_by_parent_email(parent_email):
initial_count = len(mock_users_db)
users_to_delete = [
email
for email, user in mock_users_db.items()
if user.get("parent_email") == parent_email
]
for email in users_to_delete:
del mock_users_db[email]
return initial_count - len(mock_users_db)
2025-11-08 18:20:25 +01:00
2025-11-08 22:05:07 +01:00
mocker.patch.object(
mock_user_service_instance,
"authenticate_user",
side_effect=mock_authenticate_user,
)
mocker.patch.object(
mock_user_service_instance,
"get_user_by_email",
side_effect=mock_get_user_by_email,
)
mocker.patch.object(
mock_user_service_instance, "create_user", side_effect=mock_create_user
)
mocker.patch.object(
mock_user_service_instance, "get_all_users", side_effect=mock_get_all_users
)
mocker.patch.object(
mock_user_service_instance, "update_user_quota", return_value=None
) # Keep as is for now
mocker.patch.object(
mock_user_service_instance, "delete_user", side_effect=mock_delete_user
)
mocker.patch.object(
mock_user_service_instance,
"get_users_by_parent_email",
side_effect=mock_get_users_by_parent_email,
)
mocker.patch.object(
mock_user_service_instance,
"delete_users_by_parent_email",
side_effect=mock_delete_users_by_parent_email,
)
mocker.patch.object(
mock_user_service_instance,
"generate_reset_token",
side_effect=mock_generate_reset_token,
)
mocker.patch.object(
mock_user_service_instance,
"get_user_by_reset_token",
side_effect=lambda token: next(
(
user
for user in mock_users_db.values()
if user.get("reset_token") == token
),
None,
),
)
mocker.patch.object(
mock_user_service_instance, "reset_password", side_effect=mock_reset_password
)
mocker.patch.object(
mock_user_service_instance,
"validate_reset_token",
side_effect=mock_validate_reset_token,
)
mocker.patch.object(
mock_user_service_instance, "_save_users", side_effect=mock_save_users
)
2025-11-08 18:20:25 +01:00
2025-11-08 22:05:07 +01:00
try:
yield client
finally:
# Clean up temporary files
users_file.unlink(missing_ok=True)
config_file.unlink(missing_ok=True) # Use missing_ok for robustness
2025-11-08 19:39:25 +01:00
2025-11-09 02:14:26 +01:00
@pytest.fixture
async def logged_in_client(aiohttp_client, create_test_app, mocker):
"""Fixture to provide an aiohttp client with a logged-in user."""
app = create_test_app
client = await aiohttp_client(app)
user_service = app["user_service"]
def mock_create_user(full_name, email, password, parent_email=None):
return {
"full_name": full_name,
"email": email,
"password": "hashed_password",
"storage_quota_gb": 10,
"storage_used_gb": 0,
"parent_email": parent_email,
"shared_items": {}
}
def mock_authenticate_user(email, password):
return {
"email": email,
"full_name": "Test User",
"is_admin": False,
"storage_quota_gb": 10,
"storage_used_gb": 0
}
def mock_get_user_by_email(email):
return {
"email": email,
"full_name": "Test User",
"is_admin": False,
"storage_quota_gb": 10,
"storage_used_gb": 0
}
mocker.patch.object(user_service, "create_user", side_effect=mock_create_user)
mocker.patch.object(user_service, "authenticate_user", side_effect=mock_authenticate_user)
mocker.patch.object(user_service, "get_user_by_email", side_effect=mock_get_user_by_email)
await client.post(
"/register",
data={
"full_name": "Test User",
"email": "test@example.com",
"password": "password",
"confirm_password": "password",
},
)
await client.post(
"/login", data={"email": "test@example.com", "password": "password"}
)
return client
@pytest.fixture
async def logged_in_admin_client(aiohttp_client, create_test_app, mocker):
"""Fixture to provide an aiohttp client with a logged-in admin user."""
app = create_test_app
client = await aiohttp_client(app)
user_service = app["user_service"]
def mock_create_user(full_name, email, password, parent_email=None):
return {
"full_name": full_name,
"email": email,
"password": "hashed_password",
"storage_quota_gb": 100,
"storage_used_gb": 0,
"parent_email": parent_email,
"shared_items": {}
}
def mock_authenticate_user(email, password):
return {
"email": email,
"full_name": "Admin User",
"is_admin": True,
"storage_quota_gb": 100,
"storage_used_gb": 0
}
def mock_get_user_by_email(email):
return {
"email": email,
"full_name": "Admin User",
"is_admin": True,
"storage_quota_gb": 100,
"storage_used_gb": 0
}
mocker.patch.object(user_service, "create_user", side_effect=mock_create_user)
mocker.patch.object(user_service, "authenticate_user", side_effect=mock_authenticate_user)
mocker.patch.object(user_service, "get_user_by_email", side_effect=mock_get_user_by_email)
await client.post(
"/register",
data={
"full_name": "Admin User",
"email": "admin@example.com",
"password": "password",
"confirm_password": "password",
},
)
await client.post(
"/login", data={"email": "admin@example.com", "password": "password"}
)
return client
2025-11-08 19:39:25 +01:00
@pytest.fixture
def mock_send_email(mocker: MockerFixture):
"""
Fixture to mock the send_email function.
This fixture will return the mock that was patched globally by the client fixture.
"""
# Access the globally patched mock
return mocker.patch("retoors.helpers.email_sender.send_email")