Files
wizarr/tests/test_wizard_drag_drop.py

571 lines
19 KiB
Python

"""
Tests for wizard step drag-and-drop functionality.
This module tests the drag-and-drop reordering of wizard steps, including:
- Reordering within the same category
- Moving steps between categories (pre-invite ↔ post-invite)
- Preventing moves between different server types
- Error handling and UI feedback
"""
import json
import pytest
from sqlalchemy.exc import IntegrityError
from app.extensions import db
from app.models import AdminAccount, Library, MediaServer, User, WizardStep
@pytest.fixture
def session(app):
"""Return a clean database session inside an app context."""
with app.app_context():
# Clean up any existing data before each test - delete in correct order for FKs
db.session.rollback()
db.session.execute(db.text("DELETE FROM invitation_server"))
db.session.execute(db.text("DELETE FROM invitation_user"))
db.session.query(User).delete()
db.session.query(WizardStep).delete()
db.session.query(Library).delete()
db.session.query(MediaServer).delete()
db.session.query(AdminAccount).delete()
db.session.commit()
yield db.session
# Clean up after the test - rollback first in case of errors
try:
db.session.rollback()
db.session.execute(db.text("DELETE FROM invitation_server"))
db.session.execute(db.text("DELETE FROM invitation_user"))
db.session.query(User).delete()
db.session.query(WizardStep).delete()
db.session.query(Library).delete()
db.session.query(MediaServer).delete()
db.session.query(AdminAccount).delete()
db.session.commit()
except Exception:
db.session.rollback()
@pytest.fixture
def admin_user(session):
"""Create an admin user for authentication."""
admin = AdminAccount(username="testadmin")
admin.set_password("testpass123")
session.add(admin)
session.commit()
return admin
def authenticate_client(client, admin_user):
"""Helper function to authenticate a client with an admin user."""
with client.session_transaction() as sess:
sess["_user_id"] = str(admin_user.id)
sess["_fresh"] = True
@pytest.fixture
def wizard_steps(session):
"""Create test wizard steps for drag-and-drop testing."""
steps = [
# Plex pre-invite steps
WizardStep(
server_type="plex",
category="pre_invite",
position=0,
title="Plex Pre Step 1",
markdown="# Pre Step 1",
),
WizardStep(
server_type="plex",
category="pre_invite",
position=1,
title="Plex Pre Step 2",
markdown="# Pre Step 2",
),
WizardStep(
server_type="plex",
category="pre_invite",
position=2,
title="Plex Pre Step 3",
markdown="# Pre Step 3",
),
# Plex post-invite steps
WizardStep(
server_type="plex",
category="post_invite",
position=0,
title="Plex Post Step 1",
markdown="# Post Step 1",
),
WizardStep(
server_type="plex",
category="post_invite",
position=1,
title="Plex Post Step 2",
markdown="# Post Step 2",
),
# Jellyfin pre-invite steps
WizardStep(
server_type="jellyfin",
category="pre_invite",
position=0,
title="Jellyfin Pre Step 1",
markdown="# Jellyfin Pre",
),
]
session.add_all(steps)
session.commit()
# Return step IDs for testing
return {
"plex_pre": [s.id for s in steps[:3]],
"plex_post": [s.id for s in steps[3:5]],
"jellyfin_pre": [steps[5].id],
}
class TestDragDropReordering:
"""Test drag-and-drop reordering functionality."""
def test_reorder_within_same_category(self, client, admin_user, wizard_steps):
"""Test reordering steps within the same category."""
# Authenticate using session
with client.session_transaction() as sess:
sess["_user_id"] = str(admin_user.id)
sess["_fresh"] = True
# Get original order
plex_pre_ids = wizard_steps["plex_pre"]
# Reverse the order
new_order = list(reversed(plex_pre_ids))
# Send reorder request
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{"server_type": "plex", "category": "pre_invite", "order": new_order}
),
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "ok"
# Verify new positions in database
steps = (
WizardStep.query.filter_by(server_type="plex", category="pre_invite")
.order_by(WizardStep.position)
.all()
)
assert len(steps) == 3
assert [s.id for s in steps] == new_order
assert steps[0].position == 0
assert steps[1].position == 1
assert steps[2].position == 2
def test_move_step_between_categories(self, client, admin_user, wizard_steps):
"""Test moving a step from pre_invite to post_invite category."""
authenticate_client(client, admin_user)
# Get a pre-invite step
step_id = wizard_steps["plex_pre"][0]
# Move it to post-invite category
plex_post_ids = wizard_steps["plex_post"]
new_order = plex_post_ids + [step_id]
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{"server_type": "plex", "category": "post_invite", "order": new_order}
),
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "ok"
# Verify step moved to post_invite category
step = db.session.get(WizardStep, step_id)
assert step is not None
assert step.category == "post_invite"
assert step.position == 2 # Last position in new category
# Verify it's no longer in pre_invite
pre_steps = WizardStep.query.filter_by(
server_type="plex", category="pre_invite"
).all()
assert len(pre_steps) == 2
assert step_id not in [s.id for s in pre_steps]
def test_reorder_updates_all_positions(self, client, admin_user, wizard_steps):
"""Test that reordering updates all step positions correctly."""
authenticate_client(client, admin_user)
# Get original order
plex_pre_ids = wizard_steps["plex_pre"]
# Create a custom order: [2, 0, 1]
new_order = [plex_pre_ids[2], plex_pre_ids[0], plex_pre_ids[1]]
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{"server_type": "plex", "category": "pre_invite", "order": new_order}
),
content_type="application/json",
)
assert response.status_code == 200
# Verify positions are sequential starting from 0
steps = (
WizardStep.query.filter_by(server_type="plex", category="pre_invite")
.order_by(WizardStep.position)
.all()
)
assert [s.id for s in steps] == new_order
assert [s.position for s in steps] == [0, 1, 2]
def test_reorder_with_invalid_category(self, client, admin_user, wizard_steps):
"""Test that reordering with invalid category returns error."""
authenticate_client(client, admin_user)
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{
"server_type": "plex",
"category": "invalid_category",
"order": wizard_steps["plex_pre"],
}
),
content_type="application/json",
)
assert response.status_code == 400
def test_reorder_with_missing_parameters(self, client, admin_user, wizard_steps):
"""Test that reordering with missing parameters returns error."""
authenticate_client(client, admin_user)
# Missing category
response = client.post(
"/settings/wizard/reorder",
data=json.dumps({"server_type": "plex", "order": wizard_steps["plex_pre"]}),
content_type="application/json",
)
assert response.status_code == 400
def test_reorder_requires_authentication(self, client, wizard_steps):
"""Test that reordering requires authentication."""
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{
"server_type": "plex",
"category": "pre_invite",
"order": wizard_steps["plex_pre"],
}
),
content_type="application/json",
)
# Should redirect to login or return 401/403
assert response.status_code in [302, 401, 403]
def test_reorder_preserves_other_categories(self, client, admin_user, wizard_steps):
"""Test that reordering one category doesn't affect other categories."""
authenticate_client(client, admin_user)
# Get original post-invite steps
original_post_steps = (
WizardStep.query.filter_by(server_type="plex", category="post_invite")
.order_by(WizardStep.position)
.all()
)
original_post_ids = [s.id for s in original_post_steps]
# Reorder pre-invite steps
plex_pre_ids = wizard_steps["plex_pre"]
new_order = list(reversed(plex_pre_ids))
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{"server_type": "plex", "category": "pre_invite", "order": new_order}
),
content_type="application/json",
)
assert response.status_code == 200
# Verify post-invite steps unchanged
post_steps = (
WizardStep.query.filter_by(server_type="plex", category="post_invite")
.order_by(WizardStep.position)
.all()
)
assert [s.id for s in post_steps] == original_post_ids
assert [s.position for s in post_steps] == [0, 1]
def test_reorder_preserves_other_servers(self, client, admin_user, wizard_steps):
"""Test that reordering one server doesn't affect other servers."""
authenticate_client(client, admin_user)
# Get original jellyfin step
original_jellyfin = WizardStep.query.filter_by(
server_type="jellyfin", category="pre_invite"
).first()
assert original_jellyfin is not None
original_jellyfin_id = original_jellyfin.id
original_jellyfin_position = original_jellyfin.position
# Reorder plex steps
plex_pre_ids = wizard_steps["plex_pre"]
new_order = list(reversed(plex_pre_ids))
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{"server_type": "plex", "category": "pre_invite", "order": new_order}
),
content_type="application/json",
)
assert response.status_code == 200
# Verify jellyfin step unchanged
jellyfin_step = db.session.get(WizardStep, original_jellyfin_id)
assert jellyfin_step is not None
assert jellyfin_step.server_type == "jellyfin"
assert jellyfin_step.category == "pre_invite"
assert jellyfin_step.position == original_jellyfin_position
class TestDragDropEdgeCases:
"""Test edge cases and error scenarios for drag-and-drop."""
def test_reorder_empty_list(self, client, admin_user):
"""Test reordering with empty order list."""
authenticate_client(client, admin_user)
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{"server_type": "plex", "category": "pre_invite", "order": []}
),
content_type="application/json",
)
# Should succeed (no-op)
assert response.status_code == 200
def test_reorder_single_step(self, client, admin_user, wizard_steps):
"""Test reordering with single step (no-op)."""
authenticate_client(client, admin_user)
# jellyfin_pre is a single-item list, get the first element
jellyfin_id = (
wizard_steps["jellyfin_pre"][0]
if isinstance(wizard_steps["jellyfin_pre"], list)
else wizard_steps["jellyfin_pre"]
)
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{
"server_type": "jellyfin",
"category": "pre_invite",
"order": [jellyfin_id],
}
),
content_type="application/json",
)
assert response.status_code == 200
# Verify step unchanged
step = db.session.get(WizardStep, jellyfin_id)
assert step is not None
assert step.position == 0
def test_reorder_with_nonexistent_step_id(self, client, admin_user, wizard_steps):
"""Test reordering with non-existent step ID."""
authenticate_client(client, admin_user)
# Use a very high ID that doesn't exist
nonexistent_id = 99999
plex_pre_ids = wizard_steps["plex_pre"]
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{
"server_type": "plex",
"category": "pre_invite",
"order": plex_pre_ids + [nonexistent_id],
}
),
content_type="application/json",
)
# Should succeed but ignore non-existent ID
assert response.status_code == 200
# Verify only valid steps were reordered
steps = (
WizardStep.query.filter_by(server_type="plex", category="pre_invite")
.order_by(WizardStep.position)
.all()
)
assert len(steps) == 3
assert all(s.id in plex_pre_ids for s in steps)
def test_reorder_with_duplicate_ids(self, client, admin_user, wizard_steps):
"""Test reordering with duplicate step IDs causes constraint violation."""
authenticate_client(client, admin_user)
plex_pre_ids = wizard_steps["plex_pre"]
# Include first ID twice - this will cause a unique constraint violation
# because the same step ID will try to have the same position twice
order_with_duplicate = [plex_pre_ids[0], plex_pre_ids[0], plex_pre_ids[1]]
# This should fail with a 500 error due to database constraint
# In a production system, this would be caught and handled gracefully
with pytest.raises(IntegrityError):
client.post(
"/settings/wizard/reorder",
data=json.dumps(
{
"server_type": "plex",
"category": "pre_invite",
"order": order_with_duplicate,
}
),
content_type="application/json",
)
def test_reorder_with_invalid_json(self, client, admin_user):
"""Test reordering with invalid JSON."""
authenticate_client(client, admin_user)
response = client.post(
"/settings/wizard/reorder",
data="invalid json",
content_type="application/json",
)
assert response.status_code == 400
def test_reorder_with_wrong_content_type(self, client, admin_user):
"""Test reordering with wrong content type."""
authenticate_client(client, admin_user)
response = client.post(
"/settings/wizard/reorder",
data="server_type=plex&category=pre_invite",
content_type="application/x-www-form-urlencoded",
)
# Should fail or handle gracefully
assert response.status_code in [400, 415]
class TestDragDropUIIntegration:
"""Test UI integration aspects of drag-and-drop."""
def test_wizard_steps_page_loads(self, client, admin_user, wizard_steps):
"""Test that wizard steps page loads successfully with steps."""
# Need wizard_steps fixture to populate the page
_ = wizard_steps
authenticate_client(client, admin_user)
response = client.get("/settings/wizard", follow_redirects=True)
assert response.status_code == 200
html = response.data.decode("utf-8")
# Check that the page contains wizard-related content
# The exact structure may vary, but we should see wizard-related elements
assert "wizard" in html.lower() or "step" in html.lower()
def test_wizard_admin_page_accessible(self, client, admin_user):
"""Test that wizard admin page is accessible to authenticated users."""
authenticate_client(client, admin_user)
response = client.get("/settings/wizard", follow_redirects=True)
assert response.status_code == 200
class TestDragDropPayloadStructure:
"""Test the structure of AJAX request payloads."""
def test_payload_includes_server_type(self, client, admin_user, wizard_steps):
"""Test that reorder payload includes server_type."""
authenticate_client(client, admin_user)
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{
"server_type": "plex",
"category": "pre_invite",
"order": wizard_steps["plex_pre"],
}
),
content_type="application/json",
)
assert response.status_code == 200
def test_payload_includes_category(self, client, admin_user, wizard_steps):
"""Test that reorder payload includes category."""
authenticate_client(client, admin_user)
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{
"server_type": "plex",
"category": "pre_invite",
"order": wizard_steps["plex_pre"],
}
),
content_type="application/json",
)
assert response.status_code == 200
def test_payload_includes_order_array(self, client, admin_user, wizard_steps):
"""Test that reorder payload includes order array."""
authenticate_client(client, admin_user)
response = client.post(
"/settings/wizard/reorder",
data=json.dumps(
{
"server_type": "plex",
"category": "pre_invite",
"order": wizard_steps["plex_pre"],
}
),
content_type="application/json",
)
assert response.status_code == 200