test: Replace pytest-asyncio plugin fixtures with anyio fixtures
This commit is contained in:
+1
-4
@@ -18,9 +18,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_test_loop_scope = "session"
|
||||
asyncio_default_fixture_loop_scope = "session"
|
||||
anyio_mode = "auto"
|
||||
log_cli = 1
|
||||
log_cli_level = "WARN"
|
||||
log_level = "WARN"
|
||||
@@ -53,7 +51,6 @@ dev = [
|
||||
"ipython>=9.2.0",
|
||||
"playwright>=1.49.1",
|
||||
"pytest>=8.3.5",
|
||||
"pytest-asyncio>=1.0.0",
|
||||
"pytest-cov>=6.1.1",
|
||||
"pytest-playwright-asyncio>=0.7.1",
|
||||
"ruff>=0.11.13",
|
||||
|
||||
@@ -9,7 +9,7 @@ logger = logging.getLogger(__name__)
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_create_and_delete_share(nc_client):
|
||||
"""Test creating and deleting a file share."""
|
||||
# Create a test user to share with
|
||||
@@ -68,7 +68,7 @@ async def test_create_and_delete_share(nc_client):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_update_share_permissions(nc_client):
|
||||
"""Test updating share permissions."""
|
||||
# Create a test user to share with
|
||||
@@ -120,7 +120,7 @@ async def test_update_share_permissions(nc_client):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_list_shares(nc_client):
|
||||
"""Test listing all shares."""
|
||||
# Create a test user to share with
|
||||
|
||||
+64
-94
@@ -15,6 +15,12 @@ from nextcloud_mcp_server.client import NextcloudClient
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def anyio_backend():
|
||||
"""Configure anyio to use asyncio backend for all tests."""
|
||||
return "asyncio"
|
||||
|
||||
|
||||
async def wait_for_nextcloud(
|
||||
host: str, max_attempts: int = 30, delay: float = 2.0
|
||||
) -> bool:
|
||||
@@ -111,7 +117,7 @@ async def create_mcp_client_session(
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def nc_client() -> AsyncGenerator[NextcloudClient, Any]:
|
||||
async def nc_client(anyio_backend) -> AsyncGenerator[NextcloudClient, Any]:
|
||||
"""
|
||||
Fixture to create a NextcloudClient instance for integration tests.
|
||||
Uses environment variables for configuration.
|
||||
@@ -146,35 +152,21 @@ async def nc_client() -> AsyncGenerator[NextcloudClient, Any]:
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def nc_mcp_client() -> AsyncGenerator[ClientSession, Any]:
|
||||
async def nc_mcp_client(anyio_backend) -> AsyncGenerator[ClientSession, Any]:
|
||||
"""
|
||||
Fixture to create an MCP client session for integration tests using streamable-http.
|
||||
|
||||
Note: This fixture uses a workaround for pytest-asyncio + anyio incompatibility.
|
||||
pytest-asyncio runs fixture teardown in a new asyncio task, which violates anyio's
|
||||
requirement that cancel scopes must be entered/exited in the same task. We catch
|
||||
and ignore these expected teardown errors while allowing real errors to propagate.
|
||||
|
||||
See: https://github.com/modelcontextprotocol/python-sdk/issues/577
|
||||
Uses anyio pytest plugin for proper async fixture handling.
|
||||
"""
|
||||
try:
|
||||
async for session in create_mcp_client_session(
|
||||
url="http://127.0.0.1:8000/mcp", client_name="Basic MCP"
|
||||
):
|
||||
yield session
|
||||
except RuntimeError as e:
|
||||
# Expected error during pytest-asyncio fixture teardown
|
||||
# pytest-asyncio creates a new task for teardown, causing:
|
||||
# "Attempted to exit cancel scope in a different task than it was entered in"
|
||||
if "cancel scope" in str(e) and "different task" in str(e):
|
||||
logger.debug(f"Ignoring expected pytest-asyncio teardown issue: {e}")
|
||||
else:
|
||||
# Unexpected RuntimeError - re-raise
|
||||
raise
|
||||
async for session in create_mcp_client_session(
|
||||
url="http://127.0.0.1:8000/mcp", client_name="Basic MCP"
|
||||
):
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def nc_mcp_oauth_client(
|
||||
anyio_backend,
|
||||
playwright_oauth_token: str,
|
||||
) -> AsyncGenerator[ClientSession, Any]:
|
||||
"""
|
||||
@@ -182,22 +174,14 @@ async def nc_mcp_oauth_client(
|
||||
Connects to the OAuth-enabled MCP server on port 8001 with OAuth authentication.
|
||||
|
||||
Uses headless browser automation suitable for CI/CD.
|
||||
|
||||
Note: Includes workaround for pytest-asyncio + anyio incompatibility.
|
||||
See nc_mcp_client fixture for details.
|
||||
Uses anyio pytest plugin for proper async fixture handling.
|
||||
"""
|
||||
try:
|
||||
async for session in create_mcp_client_session(
|
||||
url="http://127.0.0.1:8001/mcp",
|
||||
token=playwright_oauth_token,
|
||||
client_name="OAuth MCP (Playwright)",
|
||||
):
|
||||
yield session
|
||||
except RuntimeError as e:
|
||||
if "cancel scope" in str(e) and "different task" in str(e):
|
||||
logger.debug(f"Ignoring expected pytest-asyncio teardown issue: {e}")
|
||||
else:
|
||||
raise
|
||||
async for session in create_mcp_client_session(
|
||||
url="http://127.0.0.1:8001/mcp",
|
||||
token=playwright_oauth_token,
|
||||
client_name="OAuth MCP (Playwright)",
|
||||
):
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -519,6 +503,7 @@ async def temporary_board_with_card(
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def nc_oauth_client(
|
||||
anyio_backend,
|
||||
playwright_oauth_token: str,
|
||||
) -> AsyncGenerator[NextcloudClient, Any]:
|
||||
"""
|
||||
@@ -641,7 +626,7 @@ def oauth_callback_server():
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def shared_oauth_client_credentials(oauth_callback_server):
|
||||
async def shared_oauth_client_credentials(anyio_backend, oauth_callback_server):
|
||||
"""
|
||||
Fixture to obtain shared OAuth client credentials that will be reused for all users.
|
||||
|
||||
@@ -702,7 +687,7 @@ async def shared_oauth_client_credentials(oauth_callback_server):
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def playwright_oauth_token(
|
||||
browser, shared_oauth_client_credentials, oauth_callback_server
|
||||
anyio_backend, browser, shared_oauth_client_credentials, oauth_callback_server
|
||||
) -> str:
|
||||
"""
|
||||
Fixture to obtain an OAuth access token using Playwright headless browser automation.
|
||||
@@ -865,7 +850,7 @@ async def playwright_oauth_token(
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def test_users_setup(nc_client: NextcloudClient):
|
||||
async def test_users_setup(anyio_backend, nc_client: NextcloudClient):
|
||||
"""
|
||||
Create test users for multi-user OAuth testing.
|
||||
|
||||
@@ -1112,7 +1097,11 @@ async def _get_oauth_token_for_user(
|
||||
# Parallel token retrieval fixture - fetches all OAuth tokens concurrently
|
||||
@pytest.fixture(scope="session")
|
||||
async def all_oauth_tokens(
|
||||
browser, shared_oauth_client_credentials, test_users_setup, oauth_callback_server
|
||||
anyio_backend,
|
||||
browser,
|
||||
shared_oauth_client_credentials,
|
||||
test_users_setup,
|
||||
oauth_callback_server,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Fetch OAuth tokens for all test users in parallel for speed.
|
||||
@@ -1172,101 +1161,82 @@ async def all_oauth_tokens(
|
||||
|
||||
# Session-scoped OAuth token fixtures - now use the parallel fixture
|
||||
@pytest.fixture(scope="session")
|
||||
async def alice_oauth_token(all_oauth_tokens) -> str:
|
||||
async def alice_oauth_token(anyio_backend, all_oauth_tokens) -> str:
|
||||
"""OAuth token for alice (cached for session). Uses shared OAuth client."""
|
||||
return all_oauth_tokens["alice"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def bob_oauth_token(all_oauth_tokens) -> str:
|
||||
async def bob_oauth_token(anyio_backend, all_oauth_tokens) -> str:
|
||||
"""OAuth token for bob (cached for session). Uses shared OAuth client."""
|
||||
return all_oauth_tokens["bob"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def charlie_oauth_token(all_oauth_tokens) -> str:
|
||||
async def charlie_oauth_token(anyio_backend, all_oauth_tokens) -> str:
|
||||
"""OAuth token for charlie (cached for session). Uses shared OAuth client."""
|
||||
return all_oauth_tokens["charlie"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def diana_oauth_token(all_oauth_tokens) -> str:
|
||||
async def diana_oauth_token(anyio_backend, all_oauth_tokens) -> str:
|
||||
"""OAuth token for diana (cached for session). Uses shared OAuth client."""
|
||||
return all_oauth_tokens["diana"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def alice_mcp_client(
|
||||
anyio_backend,
|
||||
alice_oauth_token: str,
|
||||
) -> AsyncGenerator[ClientSession, Any]:
|
||||
"""MCP client authenticated as alice (owner role)."""
|
||||
try:
|
||||
async for session in create_mcp_client_session(
|
||||
url="http://127.0.0.1:8001/mcp",
|
||||
token=alice_oauth_token,
|
||||
client_name="Alice MCP",
|
||||
):
|
||||
yield session
|
||||
except RuntimeError as e:
|
||||
if "cancel scope" in str(e) and "different task" in str(e):
|
||||
logger.debug(f"Ignoring expected pytest-asyncio teardown issue: {e}")
|
||||
else:
|
||||
raise
|
||||
async for session in create_mcp_client_session(
|
||||
url="http://127.0.0.1:8001/mcp",
|
||||
token=alice_oauth_token,
|
||||
client_name="Alice MCP",
|
||||
):
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def bob_mcp_client(bob_oauth_token: str) -> AsyncGenerator[ClientSession, Any]:
|
||||
async def bob_mcp_client(
|
||||
anyio_backend, bob_oauth_token: str
|
||||
) -> AsyncGenerator[ClientSession, Any]:
|
||||
"""MCP client authenticated as bob (viewer role)."""
|
||||
try:
|
||||
async for session in create_mcp_client_session(
|
||||
url="http://127.0.0.1:8001/mcp",
|
||||
token=bob_oauth_token,
|
||||
client_name="Bob MCP",
|
||||
):
|
||||
yield session
|
||||
except RuntimeError as e:
|
||||
if "cancel scope" in str(e) and "different task" in str(e):
|
||||
logger.debug(f"Ignoring expected pytest-asyncio teardown issue: {e}")
|
||||
else:
|
||||
raise
|
||||
async for session in create_mcp_client_session(
|
||||
url="http://127.0.0.1:8001/mcp",
|
||||
token=bob_oauth_token,
|
||||
client_name="Bob MCP",
|
||||
):
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def charlie_mcp_client(
|
||||
anyio_backend,
|
||||
charlie_oauth_token: str,
|
||||
) -> AsyncGenerator[ClientSession, Any]:
|
||||
"""MCP client authenticated as charlie (editor role, in 'editors' group)."""
|
||||
try:
|
||||
async for session in create_mcp_client_session(
|
||||
url="http://127.0.0.1:8001/mcp",
|
||||
token=charlie_oauth_token,
|
||||
client_name="Charlie MCP",
|
||||
):
|
||||
yield session
|
||||
except RuntimeError as e:
|
||||
if "cancel scope" in str(e) and "different task" in str(e):
|
||||
logger.debug(f"Ignoring expected pytest-asyncio teardown issue: {e}")
|
||||
else:
|
||||
raise
|
||||
async for session in create_mcp_client_session(
|
||||
url="http://127.0.0.1:8001/mcp",
|
||||
token=charlie_oauth_token,
|
||||
client_name="Charlie MCP",
|
||||
):
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def diana_mcp_client(
|
||||
anyio_backend,
|
||||
diana_oauth_token: str,
|
||||
) -> AsyncGenerator[ClientSession, Any]:
|
||||
"""MCP client authenticated as diana (no-access role)."""
|
||||
try:
|
||||
async for session in create_mcp_client_session(
|
||||
url="http://127.0.0.1:8001/mcp",
|
||||
token=diana_oauth_token,
|
||||
client_name="Diana MCP",
|
||||
):
|
||||
yield session
|
||||
except RuntimeError as e:
|
||||
if "cancel scope" in str(e) and "different task" in str(e):
|
||||
logger.debug(f"Ignoring expected pytest-asyncio teardown issue: {e}")
|
||||
else:
|
||||
raise
|
||||
async for session in create_mcp_client_session(
|
||||
url="http://127.0.0.1:8001/mcp",
|
||||
token=diana_oauth_token,
|
||||
client_name="Diana MCP",
|
||||
):
|
||||
yield session
|
||||
|
||||
|
||||
# Test user/group fixtures for clean test isolation
|
||||
|
||||
@@ -46,7 +46,7 @@ async def delete_board_acl(nc_client, board_id: int, acl_id: int):
|
||||
logger.info(f"Deleted ACL {acl_id} from board {board_id}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_deck_board_view_permissions(
|
||||
nc_client, alice_mcp_client, bob_mcp_client, diana_mcp_client
|
||||
):
|
||||
@@ -119,7 +119,7 @@ async def test_deck_board_view_permissions(
|
||||
await nc_client.deck.delete_board(board_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_deck_board_edit_permissions(
|
||||
nc_client, alice_mcp_client, charlie_mcp_client, bob_mcp_client
|
||||
):
|
||||
@@ -214,7 +214,7 @@ async def test_deck_board_edit_permissions(
|
||||
await nc_client.deck.delete_board(board_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_deck_board_manage_permissions(
|
||||
nc_client, alice_mcp_client, charlie_mcp_client
|
||||
):
|
||||
@@ -289,7 +289,7 @@ async def test_deck_board_manage_permissions(
|
||||
await nc_client.deck.delete_board(board_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_deck_user_isolation(nc_client, alice_mcp_client, bob_mcp_client):
|
||||
"""
|
||||
Test that users can only see their own boards when not shared.
|
||||
|
||||
@@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.oauth]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_file_share_read_permissions(
|
||||
alice_mcp_client, bob_mcp_client, diana_mcp_client
|
||||
):
|
||||
@@ -104,7 +104,7 @@ async def test_file_share_read_permissions(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_file_share_write_permissions(
|
||||
alice_mcp_client, charlie_mcp_client, bob_mcp_client
|
||||
):
|
||||
@@ -210,7 +210,7 @@ async def test_file_share_write_permissions(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_file_list_permissions(alice_mcp_client, bob_mcp_client):
|
||||
"""
|
||||
Test that file listing respects share permissions.
|
||||
@@ -326,7 +326,7 @@ async def test_file_list_permissions(alice_mcp_client, bob_mcp_client):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_folder_share_permissions(alice_mcp_client, bob_mcp_client):
|
||||
"""
|
||||
Test that folder sharing works correctly.
|
||||
|
||||
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.oauth]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_notes_share_read_permissions(
|
||||
nc_client, alice_mcp_client, bob_mcp_client, diana_mcp_client
|
||||
):
|
||||
@@ -82,7 +82,7 @@ async def test_notes_share_read_permissions(
|
||||
await nc_client.notes.delete_note(note_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_notes_share_write_permissions(
|
||||
nc_client, alice_mcp_client, charlie_mcp_client, bob_mcp_client
|
||||
):
|
||||
@@ -149,7 +149,7 @@ async def test_notes_share_write_permissions(
|
||||
await nc_client.notes.delete_note(note_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_user_isolation_notes(nc_client, alice_mcp_client, bob_mcp_client):
|
||||
"""
|
||||
Test that users can only see their own notes when not shared.
|
||||
@@ -222,7 +222,7 @@ async def test_user_isolation_notes(nc_client, alice_mcp_client, bob_mcp_client)
|
||||
await nc_client.notes.delete_note(bob_note_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_oauth_mcp_clients_initialized(
|
||||
alice_mcp_client, bob_mcp_client, charlie_mcp_client, diana_mcp_client
|
||||
):
|
||||
|
||||
@@ -3,7 +3,7 @@ import pytest
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_create_and_delete_user(nc_client: NextcloudClient, test_user):
|
||||
"""Test creating a user and verifying deletion (cleanup by fixture)."""
|
||||
user_config = test_user
|
||||
@@ -29,7 +29,7 @@ async def test_create_and_delete_user(nc_client: NextcloudClient, test_user):
|
||||
# Note: Fixture cleanup will also try to delete but handle 404 gracefully
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_update_user_field(nc_client: NextcloudClient, test_user):
|
||||
"""Test updating user fields."""
|
||||
user_config = test_user
|
||||
@@ -44,7 +44,7 @@ async def test_update_user_field(nc_client: NextcloudClient, test_user):
|
||||
# Fixture will handle cleanup
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_user_groups(nc_client: NextcloudClient, test_user_in_group):
|
||||
"""Test adding and removing users from groups."""
|
||||
user_config, groupid = test_user_in_group
|
||||
@@ -61,7 +61,7 @@ async def test_user_groups(nc_client: NextcloudClient, test_user_in_group):
|
||||
# Fixtures will handle cleanup
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_user_subadmins(nc_client: NextcloudClient, test_user, test_group):
|
||||
"""Test promoting and demoting subadmins."""
|
||||
user_config = test_user
|
||||
@@ -82,7 +82,7 @@ async def test_user_subadmins(nc_client: NextcloudClient, test_user, test_group)
|
||||
# Fixtures will handle cleanup
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_disable_enable_user(nc_client: NextcloudClient, test_user):
|
||||
"""Test disabling and enabling users."""
|
||||
user_config = test_user
|
||||
@@ -102,7 +102,7 @@ async def test_disable_enable_user(nc_client: NextcloudClient, test_user):
|
||||
# Fixture will handle cleanup
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_get_editable_user_fields(nc_client: NextcloudClient):
|
||||
editable_fields = await nc_client.users.get_editable_user_fields()
|
||||
assert "displayname" in editable_fields
|
||||
|
||||
@@ -648,7 +648,6 @@ dev = [
|
||||
{ name = "ipython" },
|
||||
{ name = "playwright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-playwright-asyncio" },
|
||||
{ name = "ruff" },
|
||||
@@ -671,7 +670,6 @@ dev = [
|
||||
{ name = "ipython", specifier = ">=9.2.0" },
|
||||
{ name = "playwright", specifier = ">=1.49.1" },
|
||||
{ name = "pytest", specifier = ">=8.3.5" },
|
||||
{ name = "pytest-asyncio", specifier = ">=1.0.0" },
|
||||
{ name = "pytest-cov", specifier = ">=6.1.1" },
|
||||
{ name = "pytest-playwright-asyncio", specifier = ">=0.7.1" },
|
||||
{ name = "ruff", specifier = ">=0.11.13" },
|
||||
|
||||
Reference in New Issue
Block a user