mirror of
https://github.com/wizarrrr/wizarr.git
synced 2025-12-23 23:59:23 -05:00
Refactor Dockerfile to exclude dev dependencies for production image; add pytest-playwright and related packages; update invitation handling and tests for improved functionality and performance
This commit is contained in:
@@ -17,8 +17,9 @@ ENV UV_LINK_MODE=copy
|
||||
COPY pyproject.toml uv.lock ./
|
||||
|
||||
# Install Python dependencies only (not project) with cache mount for speed
|
||||
# Exclude dev dependencies for production image
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked --no-install-project
|
||||
uv sync --locked --no-install-project --no-dev
|
||||
|
||||
# Copy npm dependency files and install with cache
|
||||
COPY app/static/package*.json ./app/static/
|
||||
@@ -32,8 +33,9 @@ COPY app/ ./app/
|
||||
COPY babel.cfg ./
|
||||
|
||||
# Install the project now that we have source code
|
||||
# Exclude dev dependencies for production image
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked
|
||||
uv sync --locked --no-dev
|
||||
|
||||
# Build translations
|
||||
RUN uv run pybabel compile -d app/translations
|
||||
|
||||
@@ -48,6 +48,20 @@ def is_invite_valid(code: str) -> tuple[bool, str]:
|
||||
return True, "okay"
|
||||
|
||||
|
||||
def _get_form_list(form: Any, key: str) -> list[str]:
|
||||
"""Get list from form, handling both WTForms and dict."""
|
||||
if hasattr(form, "getlist"):
|
||||
# WTForms object
|
||||
return form.getlist(key) or []
|
||||
# Regular dict
|
||||
value = form.get(key, [])
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
if value:
|
||||
return [str(value)]
|
||||
return []
|
||||
|
||||
|
||||
def create_invite(form: Any) -> Invitation:
|
||||
"""Takes a WTForms or dict-like `form` with the same keys as your old version."""
|
||||
# generate or validate provided code
|
||||
@@ -69,7 +83,7 @@ def create_invite(form: Any) -> Invitation:
|
||||
|
||||
# ── servers ────────────────────────────────────────────────────────────
|
||||
# Get selected server IDs from checkboxes
|
||||
server_ids = form.getlist("server_ids") or []
|
||||
server_ids = _get_form_list(form, "server_ids")
|
||||
|
||||
if not server_ids:
|
||||
# No servers selected - this is now an error condition
|
||||
@@ -131,7 +145,9 @@ def create_invite(form: Any) -> Invitation:
|
||||
invite.servers.extend(servers)
|
||||
|
||||
# Wire up library associations
|
||||
selected = form.getlist("libraries") # these are now library IDs (not external_ids)
|
||||
selected = _get_form_list(
|
||||
form, "libraries"
|
||||
) # these are now library IDs (not external_ids)
|
||||
if selected:
|
||||
# Convert string IDs to integers and filter out invalid values
|
||||
try:
|
||||
|
||||
@@ -40,6 +40,7 @@ dev = [
|
||||
"pytest>=8.3.5",
|
||||
"pytest-flask>=1.3.0",
|
||||
"pytest-mock>=3.14.1",
|
||||
"pytest-playwright>=0.6.2",
|
||||
"pyright>=1.1.400",
|
||||
"pre-commit>=4.0.1",
|
||||
"playwright>=1.54.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[pytest]
|
||||
minversion = 6.0
|
||||
addopts = -ra -q
|
||||
addopts = -ra -q --tb=short
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_files = test_*.py
|
||||
# Playwright will be enabled via conftest.py for E2E tests only
|
||||
@@ -68,7 +68,7 @@ class TestInvitationUserJourney:
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
# Navigate to invitation page
|
||||
page.goto(f"{live_server.url}{invitation_setup['invitation_url']}")
|
||||
page.goto(f"{live_server.url()()}{invitation_setup['invitation_url']}")
|
||||
|
||||
# Verify invitation page loads
|
||||
expect(page.locator("h1")).to_contain_text("Join Test Jellyfin Server")
|
||||
@@ -114,7 +114,7 @@ class TestInvitationUserJourney:
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
# Navigate to invitation page
|
||||
page.goto(f"{live_server.url}{invitation_setup['invitation_url']}")
|
||||
page.goto(f"{live_server.url()()}{invitation_setup['invitation_url']}")
|
||||
|
||||
# Test empty form submission
|
||||
page.click("button[type='submit']")
|
||||
@@ -166,7 +166,7 @@ class TestInvitationUserJourney:
|
||||
db.session.commit()
|
||||
|
||||
# Navigate to expired invitation
|
||||
page.goto(f"{live_server.url}/j/EXPIRED123")
|
||||
page.goto(f"{live_server.url()}/j/EXPIRED123")
|
||||
|
||||
# Should show expiry error
|
||||
expect(page.locator("body")).to_contain_text("expired", ignore_case=True)
|
||||
@@ -177,7 +177,7 @@ class TestInvitationUserJourney:
|
||||
def test_invalid_invitation_code(self, page: Page, live_server):
|
||||
"""Test that invalid invitation codes show 404 or error page."""
|
||||
# Navigate to non-existent invitation
|
||||
page.goto(f"{live_server.url}/j/INVALIDCODE123")
|
||||
page.goto(f"{live_server.url()}/j/INVALIDCODE123")
|
||||
|
||||
# Should show error (either 404 or invalid invitation message)
|
||||
page.wait_for_load_state("networkidle")
|
||||
@@ -205,7 +205,7 @@ class TestInvitationUserJourney:
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
# Navigate to invitation page
|
||||
page.goto(f"{live_server.url}{invitation_setup['invitation_url']}")
|
||||
page.goto(f"{live_server.url()()}{invitation_setup['invitation_url']}")
|
||||
|
||||
# Fill and submit form
|
||||
page.fill("input[name='username']", "erroruser")
|
||||
@@ -266,7 +266,7 @@ class TestMultiServerInvitationFlow:
|
||||
mock_get_client.side_effect = get_client_side_effect
|
||||
|
||||
# Navigate to invitation page
|
||||
page.goto(f"{live_server.url}/j/MULTI123")
|
||||
page.goto(f"{live_server.url()}/j/MULTI123")
|
||||
|
||||
# Should show multi-server invitation info
|
||||
expect(page.locator("body")).to_contain_text("Jellyfin Server")
|
||||
@@ -332,7 +332,7 @@ class TestMultiServerInvitationFlow:
|
||||
mock_get_client.side_effect = get_client_side_effect
|
||||
|
||||
# Navigate and submit
|
||||
page.goto(f"{live_server.url}/j/PARTIAL123")
|
||||
page.goto(f"{live_server.url()}/j/PARTIAL123")
|
||||
page.fill("input[name='username']", "partialuser")
|
||||
page.fill("input[name='password']", "testpass123")
|
||||
page.fill("input[name='confirm']", "testpass123")
|
||||
@@ -358,7 +358,7 @@ class TestInvitationUIComponents:
|
||||
self, page: Page, live_server, invitation_setup
|
||||
):
|
||||
"""Test basic accessibility of invitation form."""
|
||||
page.goto(f"{live_server.url}{invitation_setup['invitation_url']}")
|
||||
page.goto(f"{live_server.url()()}{invitation_setup['invitation_url']}")
|
||||
|
||||
# Check for form labels
|
||||
expect(
|
||||
@@ -383,7 +383,7 @@ class TestInvitationUIComponents:
|
||||
"""Test invitation page on different screen sizes."""
|
||||
# Test desktop
|
||||
page.set_viewport_size({"width": 1920, "height": 1080})
|
||||
page.goto(f"{live_server.url}{invitation_setup['invitation_url']}")
|
||||
page.goto(f"{live_server.url()()}{invitation_setup['invitation_url']}")
|
||||
expect(page.locator("form")).to_be_visible()
|
||||
|
||||
# Test tablet
|
||||
|
||||
@@ -65,7 +65,25 @@ class MockJellyfinClient:
|
||||
def __init__(self, url: str = None, token: str = None, **kwargs):
|
||||
self.url = url or "http://localhost:8096"
|
||||
self.token = token or "mock-api-key"
|
||||
self.server_id = kwargs.get("server_id", 1)
|
||||
self.server_id = kwargs.get("server_id")
|
||||
|
||||
def _create_user_with_identity_linking(self, user_kwargs: dict):
|
||||
"""Mock version of the database user creation."""
|
||||
from app.extensions import db
|
||||
from app.models import User
|
||||
|
||||
# Check if this is part of a multi-server invitation
|
||||
code = user_kwargs.get("code")
|
||||
if code:
|
||||
existing_user = User.query.filter_by(code=code).first()
|
||||
if existing_user and existing_user.identity_id:
|
||||
# Link to existing identity from same invitation
|
||||
user_kwargs["identity_id"] = existing_user.identity_id
|
||||
|
||||
new_user = User(**user_kwargs)
|
||||
db.session.add(new_user)
|
||||
db.session.flush() # Get the ID immediately
|
||||
return new_user
|
||||
|
||||
def validate_connection(self) -> tuple[bool, str]:
|
||||
"""Simulate connection validation."""
|
||||
@@ -158,6 +176,12 @@ class MockJellyfinClient:
|
||||
self, username: str, password: str, confirm: str, email: str, code: str
|
||||
) -> tuple[bool, str]:
|
||||
"""Mock implementation of invitation join process."""
|
||||
# Check connection health first
|
||||
if not mock_state.connection_healthy:
|
||||
return False, "Connection failed"
|
||||
if not mock_state.api_key_valid:
|
||||
return False, "Invalid API key"
|
||||
|
||||
# Basic validation
|
||||
if password != confirm:
|
||||
return False, "Passwords do not match"
|
||||
@@ -167,16 +191,36 @@ class MockJellyfinClient:
|
||||
return False, f"Failed to create user {username}"
|
||||
|
||||
try:
|
||||
# Simulate user creation
|
||||
# Simulate user creation in mock state
|
||||
user_id = self.create_user(username, password)
|
||||
|
||||
# Set email
|
||||
mock_state.users[user_id].email = email
|
||||
|
||||
# Set default libraries (simulate library assignment)
|
||||
enabled_libs = [
|
||||
lib_id for lib_id, lib in mock_state.libraries.items() if lib.enabled
|
||||
]
|
||||
# Set libraries based on invitation (like real clients do)
|
||||
from app.models import Invitation, Library
|
||||
|
||||
inv = Invitation.query.filter_by(code=code).first()
|
||||
if inv and inv.libraries:
|
||||
# Use invitation-specific libraries
|
||||
enabled_libs = [
|
||||
lib.external_id
|
||||
for lib in inv.libraries
|
||||
if lib.server_id == self.server_id
|
||||
]
|
||||
else:
|
||||
# Use all enabled libraries for server (fallback)
|
||||
enabled_libs = (
|
||||
[
|
||||
lib.external_id
|
||||
for lib in Library.query.filter_by(
|
||||
enabled=True, server_id=self.server_id
|
||||
).all()
|
||||
]
|
||||
if self.server_id
|
||||
else list(mock_state.libraries.keys())
|
||||
)
|
||||
|
||||
self._set_specific_folders(user_id, enabled_libs)
|
||||
|
||||
# Set default policy
|
||||
@@ -187,6 +231,24 @@ class MockJellyfinClient:
|
||||
}
|
||||
self.set_policy(user_id, default_policy)
|
||||
|
||||
# Create user in database (like real clients do)
|
||||
from app.models import Invitation
|
||||
from app.services.expiry import calculate_user_expiry
|
||||
|
||||
inv = Invitation.query.filter_by(code=code).first()
|
||||
expires = calculate_user_expiry(inv, self.server_id) if inv else None
|
||||
|
||||
self._create_user_with_identity_linking(
|
||||
{
|
||||
"username": username,
|
||||
"email": email,
|
||||
"token": user_id,
|
||||
"code": code,
|
||||
"expires": expires,
|
||||
"server_id": self.server_id,
|
||||
}
|
||||
)
|
||||
|
||||
return True, ""
|
||||
|
||||
except Exception as e:
|
||||
@@ -205,7 +267,25 @@ class MockPlexClient:
|
||||
def __init__(self, url: str = None, token: str = None, **kwargs):
|
||||
self.url = url or "http://localhost:32400"
|
||||
self.token = token or "mock-plex-token"
|
||||
self.server_id = kwargs.get("server_id", 2)
|
||||
self.server_id = kwargs.get("server_id")
|
||||
|
||||
def _create_user_with_identity_linking(self, user_kwargs: dict):
|
||||
"""Mock version of the database user creation."""
|
||||
from app.extensions import db
|
||||
from app.models import User
|
||||
|
||||
# Check if this is part of a multi-server invitation
|
||||
code = user_kwargs.get("code")
|
||||
if code:
|
||||
existing_user = User.query.filter_by(code=code).first()
|
||||
if existing_user and existing_user.identity_id:
|
||||
# Link to existing identity from same invitation
|
||||
user_kwargs["identity_id"] = existing_user.identity_id
|
||||
|
||||
new_user = User(**user_kwargs)
|
||||
db.session.add(new_user)
|
||||
db.session.flush() # Get the ID immediately
|
||||
return new_user
|
||||
|
||||
def validate_connection(self) -> tuple[bool, str]:
|
||||
"""Simulate connection validation."""
|
||||
@@ -225,6 +305,12 @@ class MockPlexClient:
|
||||
self, username: str, password: str, confirm: str, email: str, code: str
|
||||
) -> tuple[bool, str]:
|
||||
"""Mock Plex invitation join process."""
|
||||
# Check connection health first
|
||||
if not mock_state.connection_healthy:
|
||||
return False, "Plex server unreachable"
|
||||
if not mock_state.api_key_valid:
|
||||
return False, "Invalid Plex token"
|
||||
|
||||
if password != confirm:
|
||||
return False, "Passwords do not match"
|
||||
if "@" not in email:
|
||||
@@ -243,6 +329,25 @@ class MockPlexClient:
|
||||
mock_state.libraries.keys()
|
||||
), # Plex gets all libraries by default
|
||||
)
|
||||
|
||||
# Create user in database (like real clients do)
|
||||
from app.models import Invitation
|
||||
from app.services.expiry import calculate_user_expiry
|
||||
|
||||
inv = Invitation.query.filter_by(code=code).first()
|
||||
expires = calculate_user_expiry(inv, self.server_id) if inv else None
|
||||
|
||||
self._create_user_with_identity_linking(
|
||||
{
|
||||
"username": username,
|
||||
"email": email,
|
||||
"token": user_id,
|
||||
"code": code,
|
||||
"expires": expires,
|
||||
"server_id": self.server_id,
|
||||
}
|
||||
)
|
||||
|
||||
return True, ""
|
||||
|
||||
except Exception as e:
|
||||
@@ -261,7 +366,25 @@ class MockAudiobookshelfClient:
|
||||
def __init__(self, url: str = None, token: str = None, **kwargs):
|
||||
self.url = url or "http://localhost:13378"
|
||||
self.token = token or "mock-abs-token"
|
||||
self.server_id = kwargs.get("server_id", 3)
|
||||
self.server_id = kwargs.get("server_id")
|
||||
|
||||
def _create_user_with_identity_linking(self, user_kwargs: dict):
|
||||
"""Mock version of the database user creation."""
|
||||
from app.extensions import db
|
||||
from app.models import User
|
||||
|
||||
# Check if this is part of a multi-server invitation
|
||||
code = user_kwargs.get("code")
|
||||
if code:
|
||||
existing_user = User.query.filter_by(code=code).first()
|
||||
if existing_user and existing_user.identity_id:
|
||||
# Link to existing identity from same invitation
|
||||
user_kwargs["identity_id"] = existing_user.identity_id
|
||||
|
||||
new_user = User(**user_kwargs)
|
||||
db.session.add(new_user)
|
||||
db.session.flush() # Get the ID immediately
|
||||
return new_user
|
||||
|
||||
def validate_connection(self) -> tuple[bool, str]:
|
||||
if not mock_state.connection_healthy:
|
||||
@@ -275,6 +398,10 @@ class MockAudiobookshelfClient:
|
||||
def _do_join(
|
||||
self, username: str, password: str, confirm: str, email: str, code: str
|
||||
) -> tuple[bool, str]:
|
||||
# Check connection health first
|
||||
if not mock_state.connection_healthy:
|
||||
return False, "Audiobookshelf server unreachable"
|
||||
|
||||
if password != confirm:
|
||||
return False, "Passwords do not match"
|
||||
if username in mock_state.create_user_failures:
|
||||
@@ -288,6 +415,25 @@ class MockAudiobookshelfClient:
|
||||
email=email,
|
||||
policy={"isActive": True, "canDownload": True},
|
||||
)
|
||||
|
||||
# Create user in database (like real clients do)
|
||||
from app.models import Invitation
|
||||
from app.services.expiry import calculate_user_expiry
|
||||
|
||||
inv = Invitation.query.filter_by(code=code).first()
|
||||
expires = calculate_user_expiry(inv, self.server_id) if inv else None
|
||||
|
||||
self._create_user_with_identity_linking(
|
||||
{
|
||||
"username": username,
|
||||
"email": email,
|
||||
"token": user_id,
|
||||
"code": code,
|
||||
"expires": expires,
|
||||
"server_id": self.server_id,
|
||||
}
|
||||
)
|
||||
|
||||
return True, ""
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -11,7 +11,7 @@ def test_debug_unlimited_invitation(app):
|
||||
"""Debug test for unlimited invitation validation."""
|
||||
with app.app_context():
|
||||
# Create used unlimited invitation
|
||||
invite = Invitation(code="UNLIMITED123", used=True, unlimited=True)
|
||||
invite = Invitation(code="UNLIMIT123", used=True, unlimited=True)
|
||||
db.session.add(invite)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class TestInvitationValidation:
|
||||
# Test validation
|
||||
is_valid, message = is_invite_valid(invite.code)
|
||||
assert is_valid
|
||||
assert message == ""
|
||||
assert message == "okay"
|
||||
|
||||
def test_expired_invitation(self, app):
|
||||
"""Test that expired invitations are rejected."""
|
||||
@@ -92,7 +92,7 @@ class TestInvitationValidation:
|
||||
"""Test that used unlimited invitations are still valid."""
|
||||
with app.app_context():
|
||||
# Create used unlimited invitation
|
||||
invite = Invitation(code="UNLIMITED12", used=True, unlimited=True)
|
||||
invite = Invitation(code="UNLIMIT123", used=True, unlimited=True)
|
||||
db.session.add(invite)
|
||||
db.session.commit()
|
||||
|
||||
@@ -113,11 +113,7 @@ class TestSingleServerInvitations:
|
||||
def test_successful_jellyfin_invitation(self, mock_get_client, app):
|
||||
"""Test successful invitation process for Jellyfin server."""
|
||||
with app.app_context():
|
||||
# Setup mock client
|
||||
mock_client = create_mock_client("jellyfin", server_id=1)
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
# Create server and invitation
|
||||
# Create server and invitation first
|
||||
server = MediaServer(
|
||||
name="Test Jellyfin",
|
||||
server_type="jellyfin",
|
||||
@@ -127,6 +123,10 @@ class TestSingleServerInvitations:
|
||||
db.session.add(server)
|
||||
db.session.flush()
|
||||
|
||||
# Setup mock client with correct server ID
|
||||
mock_client = create_mock_client("jellyfin", server_id=server.id)
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
invite = Invitation(
|
||||
code="JELLYFIN12",
|
||||
duration="30",
|
||||
@@ -168,9 +168,6 @@ class TestSingleServerInvitations:
|
||||
def test_plex_invitation_with_oauth(self, mock_get_client, app):
|
||||
"""Test Plex invitation that might use OAuth flow."""
|
||||
with app.app_context():
|
||||
mock_client = create_mock_client("plex", server_id=2)
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
# Create Plex server
|
||||
server = MediaServer(
|
||||
name="Test Plex",
|
||||
@@ -181,6 +178,9 @@ class TestSingleServerInvitations:
|
||||
db.session.add(server)
|
||||
db.session.flush()
|
||||
|
||||
mock_client = create_mock_client("plex", server_id=server.id)
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
invite = Invitation(
|
||||
code="PLEX123",
|
||||
plex_home=True,
|
||||
@@ -215,12 +215,6 @@ class TestSingleServerInvitations:
|
||||
def test_server_connection_failure(self, mock_get_client, app):
|
||||
"""Test handling of server connection failures."""
|
||||
with app.app_context():
|
||||
# Simulate connection failure
|
||||
simulate_server_failure()
|
||||
|
||||
mock_client = create_mock_client("jellyfin", server_id=1)
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
# Create server and invitation
|
||||
server = MediaServer(
|
||||
name="Failing Server",
|
||||
@@ -231,6 +225,12 @@ class TestSingleServerInvitations:
|
||||
db.session.add(server)
|
||||
db.session.flush()
|
||||
|
||||
# Simulate connection failure
|
||||
simulate_server_failure()
|
||||
|
||||
mock_client = create_mock_client("jellyfin", server_id=server.id)
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
invite = Invitation(code="FAIL123")
|
||||
invite.servers = [server]
|
||||
db.session.add(invite)
|
||||
@@ -258,12 +258,6 @@ class TestSingleServerInvitations:
|
||||
def test_user_creation_failure(self, mock_get_client, app):
|
||||
"""Test handling when user creation fails on media server."""
|
||||
with app.app_context():
|
||||
# Simulate user creation failure
|
||||
simulate_user_creation_failure(["baduser"])
|
||||
|
||||
mock_client = create_mock_client("jellyfin", server_id=1)
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
# Create server and invitation
|
||||
server = MediaServer(
|
||||
name="Test Server",
|
||||
@@ -274,6 +268,12 @@ class TestSingleServerInvitations:
|
||||
db.session.add(server)
|
||||
db.session.flush()
|
||||
|
||||
# Simulate user creation failure
|
||||
simulate_user_creation_failure(["baduser"])
|
||||
|
||||
mock_client = create_mock_client("jellyfin", server_id=server.id)
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
invite = Invitation(code="BADUSER123")
|
||||
invite.servers = [server]
|
||||
db.session.add(invite)
|
||||
@@ -591,7 +591,9 @@ class TestInvitationExpiry:
|
||||
assert db_user.expires is not None
|
||||
|
||||
# Should expire in approximately 7 days
|
||||
expected_expiry = datetime.now(UTC) + timedelta(days=7)
|
||||
expected_expiry = datetime.now() + timedelta(
|
||||
days=7
|
||||
) # Use naive datetime like the database
|
||||
time_diff = abs((db_user.expires - expected_expiry).total_seconds())
|
||||
assert time_diff < 60 # Within 1 minute of expected
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class TestInvitationPerformance:
|
||||
"""Setup for each test."""
|
||||
setup_mock_servers()
|
||||
|
||||
@patch("app.services.media.service.get_client_for_media_server")
|
||||
@patch("app.services.invitation_manager.get_client_for_media_server")
|
||||
def test_single_invitation_processing_time(self, mock_get_client, app):
|
||||
"""Test time to process a single invitation."""
|
||||
with app.app_context():
|
||||
@@ -74,7 +74,7 @@ class TestInvitationPerformance:
|
||||
f"Processing took {processing_time:.3f}s, expected < 1.0s"
|
||||
)
|
||||
|
||||
@patch("app.services.media.service.get_client_for_media_server")
|
||||
@patch("app.services.invitation_manager.get_client_for_media_server")
|
||||
def test_concurrent_invitation_processing(self, mock_get_client, app):
|
||||
"""Test processing multiple invitations concurrently."""
|
||||
with app.app_context():
|
||||
@@ -141,7 +141,7 @@ class TestInvitationPerformance:
|
||||
created_users = get_mock_state().users
|
||||
assert len(created_users) == num_users
|
||||
|
||||
@patch("app.services.media.service.get_client_for_media_server")
|
||||
@patch("app.services.invitation_manager.get_client_for_media_server")
|
||||
def test_multi_server_invitation_performance(self, mock_get_client, app):
|
||||
"""Test performance of multi-server invitation processing."""
|
||||
with app.app_context():
|
||||
@@ -207,7 +207,7 @@ class TestInvitationLoadTesting:
|
||||
"""Setup for each test."""
|
||||
setup_mock_servers()
|
||||
|
||||
@patch("app.services.media.service.get_client_for_media_server")
|
||||
@patch("app.services.invitation_manager.get_client_for_media_server")
|
||||
def test_invitation_validation_performance(self, mock_get_client, app):
|
||||
"""Test performance of invitation validation under load."""
|
||||
with app.app_context():
|
||||
@@ -255,7 +255,7 @@ class TestInvitationLoadTesting:
|
||||
)
|
||||
assert max_time < 0.05, f"Max validation time {max_time:.4f}s too slow"
|
||||
|
||||
@patch("app.services.media.service.get_client_for_media_server")
|
||||
@patch("app.services.invitation_manager.get_client_for_media_server")
|
||||
def test_database_performance_under_load(self, mock_get_client, app):
|
||||
"""Test database performance with many invitation records."""
|
||||
with app.app_context():
|
||||
@@ -316,7 +316,7 @@ class TestInvitationLoadTesting:
|
||||
class TestInvitationMemoryUsage:
|
||||
"""Test memory usage during invitation processing."""
|
||||
|
||||
@patch("app.services.media.service.get_client_for_media_server")
|
||||
@patch("app.services.invitation_manager.get_client_for_media_server")
|
||||
def test_memory_efficient_processing(self, mock_get_client, app):
|
||||
"""Test that invitation processing doesn't leak memory."""
|
||||
import gc
|
||||
@@ -375,7 +375,7 @@ class TestInvitationErrorRecovery:
|
||||
"""Setup for each test."""
|
||||
setup_mock_servers()
|
||||
|
||||
@patch("app.services.media.service.get_client_for_media_server")
|
||||
@patch("app.services.invitation_manager.get_client_for_media_server")
|
||||
def test_recovery_from_server_failures(self, mock_get_client, app):
|
||||
"""Test system recovery when servers fail under load."""
|
||||
with app.app_context():
|
||||
|
||||
51
uv.lock
generated
51
uv.lock
generated
@@ -840,6 +840,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-base-url"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/1a/b64ac368de6b993135cb70ca4e5d958a5c268094a3a2a4cac6f0021b6c4f/pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45", size = 6702 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/1c/b00940ab9eb8ede7897443b771987f2f4a76f06be02f1b3f01eb7567e24a/pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6", size = 5302 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-flask"
|
||||
version = "1.3.0"
|
||||
@@ -866,6 +879,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-playwright"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "playwright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-base-url" },
|
||||
{ name = "python-slugify" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/47/38e292ad92134a00ea05e6fc4fc44577baaa38b0922ab7ea56312b7a6663/pytest_playwright-0.7.0.tar.gz", hash = "sha256:b3f2ea514bbead96d26376fac182f68dcd6571e7cb41680a89ff1673c05d60b6", size = 16666 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/96/5f8a4545d783674f3de33f0ebc4db16cc76ce77a4c404d284f43f09125e3/pytest_playwright-0.7.0-py3-none-any.whl", hash = "sha256:2516d0871fa606634bfe32afbcc0342d68da2dbff97fe3459849e9c428486da2", size = 16618 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
@@ -899,6 +927,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/87/3c8da047b3ec5f99511d1b4d7a5bc72d4b98751c7e78492d14dc736319c5/python_frontmatter-1.1.0-py3-none-any.whl", hash = "sha256:335465556358d9d0e6c98bbeb69b1c969f2a4a21360587b9873bfc3b213407c1", size = 9834 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-slugify"
|
||||
version = "8.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "text-unidecode" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2025.2"
|
||||
@@ -1109,6 +1149,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/55/ba2546ab09a6adebc521bf3974440dc1d8c06ed342cceb30ed62a8858835/sqlalchemy-2.0.42-py3-none-any.whl", hash = "sha256:defcdff7e661f0043daa381832af65d616e060ddb54d3fe4476f51df7eaa1835", size = 1922072 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "text-unidecode"
|
||||
version = "1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ty"
|
||||
version = "0.0.1a17"
|
||||
@@ -1257,6 +1306,7 @@ dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-flask" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-playwright" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -1299,6 +1349,7 @@ dev = [
|
||||
{ name = "pytest", specifier = ">=8.3.5" },
|
||||
{ name = "pytest-flask", specifier = ">=1.3.0" },
|
||||
{ name = "pytest-mock", specifier = ">=3.14.1" },
|
||||
{ name = "pytest-playwright", specifier = ">=0.6.2" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user