test: Add tests for sharing/groups
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
"""Integration tests for Nextcloud Sharing API client."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_and_delete_share(nc_client):
|
||||||
|
"""Test creating and deleting a file share."""
|
||||||
|
# Create a test user to share with
|
||||||
|
test_user = "testuser3"
|
||||||
|
try:
|
||||||
|
await nc_client.users.create_user(userid=test_user, password="password123")
|
||||||
|
except Exception:
|
||||||
|
pass # User might already exist
|
||||||
|
|
||||||
|
# Create a test file
|
||||||
|
file_path = "/test_share_file.txt"
|
||||||
|
file_content = b"Test file for sharing"
|
||||||
|
|
||||||
|
await nc_client.webdav.write_file(file_path, file_content)
|
||||||
|
|
||||||
|
share_id = None
|
||||||
|
try:
|
||||||
|
# Create a share
|
||||||
|
share_data = await nc_client.sharing.create_share(
|
||||||
|
path=file_path,
|
||||||
|
share_with=test_user, # Share with test user
|
||||||
|
share_type=0, # User share
|
||||||
|
permissions=1, # Read-only
|
||||||
|
)
|
||||||
|
|
||||||
|
assert share_data is not None
|
||||||
|
assert "id" in share_data
|
||||||
|
share_id = share_data["id"]
|
||||||
|
logger.info(f"Created share: {share_id}")
|
||||||
|
|
||||||
|
# Get share info
|
||||||
|
share_info = await nc_client.sharing.get_share(share_id)
|
||||||
|
assert share_info["id"] == share_id
|
||||||
|
assert share_info["path"] == file_path
|
||||||
|
assert share_info["permissions"] == 1
|
||||||
|
|
||||||
|
# List shares
|
||||||
|
shares = await nc_client.sharing.list_shares(path=file_path)
|
||||||
|
assert len(shares) > 0
|
||||||
|
assert any(s["id"] == share_id for s in shares)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
if share_id:
|
||||||
|
await nc_client.sharing.delete_share(share_id)
|
||||||
|
logger.info(f"Deleted share: {share_id}")
|
||||||
|
|
||||||
|
await nc_client.webdav.delete_resource(file_path)
|
||||||
|
|
||||||
|
# Cleanup test user
|
||||||
|
try:
|
||||||
|
await nc_client.users.delete_user(test_user)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_share_permissions(nc_client):
|
||||||
|
"""Test updating share permissions."""
|
||||||
|
# Create a test user to share with
|
||||||
|
test_user = "testuser3"
|
||||||
|
try:
|
||||||
|
await nc_client.users.create_user(userid=test_user, password="password123")
|
||||||
|
except Exception:
|
||||||
|
pass # User might already exist
|
||||||
|
|
||||||
|
# Create a test file
|
||||||
|
file_path = "/test_share_update.txt"
|
||||||
|
file_content = b"Test file for permission updates"
|
||||||
|
|
||||||
|
await nc_client.webdav.write_file(file_path, file_content)
|
||||||
|
|
||||||
|
share_id = None
|
||||||
|
try:
|
||||||
|
# Create a share with read-only permissions
|
||||||
|
share_data = await nc_client.sharing.create_share(
|
||||||
|
path=file_path,
|
||||||
|
share_with=test_user,
|
||||||
|
share_type=0,
|
||||||
|
permissions=1, # Read-only
|
||||||
|
)
|
||||||
|
share_id = share_data["id"]
|
||||||
|
|
||||||
|
# Update to read+write permissions
|
||||||
|
updated_share = await nc_client.sharing.update_share(
|
||||||
|
share_id=share_id,
|
||||||
|
permissions=3, # Read + Write
|
||||||
|
)
|
||||||
|
|
||||||
|
assert updated_share["id"] == share_id
|
||||||
|
assert updated_share["permissions"] == 3
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
if share_id:
|
||||||
|
await nc_client.sharing.delete_share(share_id)
|
||||||
|
|
||||||
|
await nc_client.webdav.delete_resource(file_path)
|
||||||
|
|
||||||
|
# Cleanup test user
|
||||||
|
try:
|
||||||
|
await nc_client.users.delete_user(test_user)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_shares(nc_client):
|
||||||
|
"""Test listing all shares."""
|
||||||
|
# Create a test user to share with
|
||||||
|
test_user = "testuser3"
|
||||||
|
try:
|
||||||
|
await nc_client.users.create_user(userid=test_user, password="password123")
|
||||||
|
except Exception:
|
||||||
|
pass # User might already exist
|
||||||
|
|
||||||
|
# Create a test file
|
||||||
|
file_path = "/test_list_shares.txt"
|
||||||
|
file_content = b"Test file for listing shares"
|
||||||
|
|
||||||
|
await nc_client.webdav.write_file(file_path, file_content)
|
||||||
|
|
||||||
|
share_id = None
|
||||||
|
try:
|
||||||
|
# Create a share
|
||||||
|
share_data = await nc_client.sharing.create_share(
|
||||||
|
path=file_path,
|
||||||
|
share_with=test_user,
|
||||||
|
share_type=0,
|
||||||
|
permissions=1,
|
||||||
|
)
|
||||||
|
share_id = share_data["id"]
|
||||||
|
|
||||||
|
# List all shares
|
||||||
|
all_shares = await nc_client.sharing.list_shares()
|
||||||
|
assert len(all_shares) > 0
|
||||||
|
|
||||||
|
# List shares for specific file
|
||||||
|
file_shares = await nc_client.sharing.list_shares(path=file_path)
|
||||||
|
assert len(file_shares) > 0
|
||||||
|
assert any(s["id"] == share_id for s in file_shares)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
if share_id:
|
||||||
|
await nc_client.sharing.delete_share(share_id)
|
||||||
|
|
||||||
|
await nc_client.webdav.delete_resource(file_path)
|
||||||
|
|
||||||
|
# Cleanup test user
|
||||||
|
try:
|
||||||
|
await nc_client.users.delete_user(test_user)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
+159
-30
@@ -1267,16 +1267,19 @@ async def _get_oauth_token_for_user(
|
|||||||
logger.info(f"Getting OAuth token for user: {username}...")
|
logger.info(f"Getting OAuth token for user: {username}...")
|
||||||
logger.info(f"Using shared OAuth client: {client_id[:16]}...")
|
logger.info(f"Using shared OAuth client: {client_id[:16]}...")
|
||||||
|
|
||||||
# Construct authorization URL
|
# Construct authorization URL with properly encoded redirect_uri
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
auth_url = (
|
auth_url = (
|
||||||
f"{authorization_endpoint}?"
|
f"{authorization_endpoint}?"
|
||||||
f"response_type=code&"
|
f"response_type=code&"
|
||||||
f"client_id={client_id}&"
|
f"client_id={client_id}&"
|
||||||
f"redirect_uri={callback_url}&"
|
f"redirect_uri={quote(callback_url, safe='')}&"
|
||||||
f"scope=openid%20profile%20email"
|
f"scope=openid%20profile%20email"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Performing browser OAuth flow for {username}...")
|
logger.info(f"Performing browser OAuth flow for {username}...")
|
||||||
|
logger.debug(f"Authorization URL: {auth_url}")
|
||||||
|
|
||||||
# Browser automation
|
# Browser automation
|
||||||
context = await browser.new_context(ignore_https_errors=True)
|
context = await browser.new_context(ignore_https_errors=True)
|
||||||
@@ -1354,49 +1357,82 @@ async def _get_oauth_token_for_user(
|
|||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
|
|
||||||
# Session-scoped OAuth token fixtures to avoid re-registering clients
|
# Parallel token retrieval fixture - fetches all OAuth tokens concurrently
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
async def alice_oauth_token(
|
async def all_oauth_tokens(
|
||||||
browser, shared_oauth_client_credentials, test_users_setup
|
browser, shared_oauth_client_credentials, test_users_setup
|
||||||
) -> str:
|
) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Fetch OAuth tokens for all test users in parallel for speed.
|
||||||
|
|
||||||
|
Returns a dict mapping username to OAuth access token.
|
||||||
|
This is significantly faster than fetching tokens sequentially.
|
||||||
|
|
||||||
|
Note: We add a small stagger between starting each flow to avoid
|
||||||
|
race conditions in Nextcloud's OAuth session handling.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
logger.info("Fetching OAuth tokens for all users in parallel...")
|
||||||
|
|
||||||
|
async def get_token_with_delay(username: str, config: dict, delay: float):
|
||||||
|
"""Get token for a user after a small delay to stagger requests."""
|
||||||
|
if delay > 0:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
return await _get_oauth_token_for_user(
|
||||||
|
browser, shared_oauth_client_credentials, username, config["password"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create tasks for all users with staggered starts (0.5s apart)
|
||||||
|
tasks = {
|
||||||
|
username: get_token_with_delay(username, config, idx * 0.5)
|
||||||
|
for idx, (username, config) in enumerate(test_users_setup.items())
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run all token fetches concurrently
|
||||||
|
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
||||||
|
|
||||||
|
# Build result dict, handling any errors
|
||||||
|
tokens = {}
|
||||||
|
for username, result in zip(tasks.keys(), results):
|
||||||
|
if isinstance(result, Exception):
|
||||||
|
logger.error(f"Failed to get OAuth token for {username}: {result}")
|
||||||
|
raise result
|
||||||
|
tokens[username] = result
|
||||||
|
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
logger.info(
|
||||||
|
f"Successfully fetched {len(tokens)} OAuth tokens in parallel "
|
||||||
|
f"in {elapsed:.1f}s (~{elapsed / len(tokens):.1f}s per user)"
|
||||||
|
)
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
# Session-scoped OAuth token fixtures - now use the parallel fixture
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
async def alice_oauth_token(all_oauth_tokens) -> str:
|
||||||
"""OAuth token for alice (cached for session). Uses shared OAuth client."""
|
"""OAuth token for alice (cached for session). Uses shared OAuth client."""
|
||||||
config = test_users_setup["alice"]
|
return all_oauth_tokens["alice"]
|
||||||
return await _get_oauth_token_for_user(
|
|
||||||
browser, shared_oauth_client_credentials, "alice", config["password"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
async def bob_oauth_token(
|
async def bob_oauth_token(all_oauth_tokens) -> str:
|
||||||
browser, shared_oauth_client_credentials, test_users_setup
|
|
||||||
) -> str:
|
|
||||||
"""OAuth token for bob (cached for session). Uses shared OAuth client."""
|
"""OAuth token for bob (cached for session). Uses shared OAuth client."""
|
||||||
config = test_users_setup["bob"]
|
return all_oauth_tokens["bob"]
|
||||||
return await _get_oauth_token_for_user(
|
|
||||||
browser, shared_oauth_client_credentials, "bob", config["password"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
async def charlie_oauth_token(
|
async def charlie_oauth_token(all_oauth_tokens) -> str:
|
||||||
browser, shared_oauth_client_credentials, test_users_setup
|
|
||||||
) -> str:
|
|
||||||
"""OAuth token for charlie (cached for session). Uses shared OAuth client."""
|
"""OAuth token for charlie (cached for session). Uses shared OAuth client."""
|
||||||
config = test_users_setup["charlie"]
|
return all_oauth_tokens["charlie"]
|
||||||
return await _get_oauth_token_for_user(
|
|
||||||
browser, shared_oauth_client_credentials, "charlie", config["password"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
async def diana_oauth_token(
|
async def diana_oauth_token(all_oauth_tokens) -> str:
|
||||||
browser, shared_oauth_client_credentials, test_users_setup
|
|
||||||
) -> str:
|
|
||||||
"""OAuth token for diana (cached for session). Uses shared OAuth client."""
|
"""OAuth token for diana (cached for session). Uses shared OAuth client."""
|
||||||
config = test_users_setup["diana"]
|
return all_oauth_tokens["diana"]
|
||||||
return await _get_oauth_token_for_user(
|
|
||||||
browser, shared_oauth_client_credentials, "diana", config["password"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -1526,3 +1562,96 @@ async def diana_mcp_client(diana_oauth_token) -> AsyncGenerator[ClientSession, A
|
|||||||
await streamable_context.__aexit__(None, None, None)
|
await streamable_context.__aexit__(None, None, None)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error closing diana streamable context: {e}")
|
logger.debug(f"Error closing diana streamable context: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# Test user/group fixtures for clean test isolation
|
||||||
|
@pytest.fixture
|
||||||
|
async def test_user(nc_client: NextcloudClient):
|
||||||
|
"""
|
||||||
|
Fixture that creates a test user and cleans it up after the test.
|
||||||
|
|
||||||
|
Returns a dict with user details that can be customized.
|
||||||
|
Usage:
|
||||||
|
async def test_something(test_user):
|
||||||
|
user_config = test_user
|
||||||
|
await nc_client.users.create_user(**user_config)
|
||||||
|
"""
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Generate unique user ID to avoid conflicts
|
||||||
|
userid = f"testuser_{uuid.uuid4().hex[:8]}"
|
||||||
|
password = "SecureTestPassword123!"
|
||||||
|
|
||||||
|
user_config = {
|
||||||
|
"userid": userid,
|
||||||
|
"password": password,
|
||||||
|
"display_name": f"Test User {userid}",
|
||||||
|
"email": f"{userid}@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup before (in case of previous failed run)
|
||||||
|
try:
|
||||||
|
await nc_client.users.delete_user(userid)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
yield user_config
|
||||||
|
|
||||||
|
# Cleanup after test
|
||||||
|
try:
|
||||||
|
await nc_client.users.delete_user(userid)
|
||||||
|
logger.debug(f"Cleaned up test user: {userid}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cleanup test user {userid}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def test_group(nc_client: NextcloudClient):
|
||||||
|
"""
|
||||||
|
Fixture that creates a test group and cleans it up after the test.
|
||||||
|
|
||||||
|
Returns the group ID.
|
||||||
|
"""
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Generate unique group ID to avoid conflicts
|
||||||
|
groupid = f"testgroup_{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
# Cleanup before (in case of previous failed run)
|
||||||
|
try:
|
||||||
|
await nc_client.groups.delete_group(groupid)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create the group
|
||||||
|
await nc_client.groups.create_group(groupid)
|
||||||
|
logger.debug(f"Created test group: {groupid}")
|
||||||
|
|
||||||
|
yield groupid
|
||||||
|
|
||||||
|
# Cleanup after test
|
||||||
|
try:
|
||||||
|
await nc_client.groups.delete_group(groupid)
|
||||||
|
logger.debug(f"Cleaned up test group: {groupid}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cleanup test group {groupid}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def test_user_in_group(nc_client: NextcloudClient, test_user, test_group):
|
||||||
|
"""
|
||||||
|
Fixture that creates a test user and adds them to a test group.
|
||||||
|
|
||||||
|
Returns a tuple of (user_config, groupid).
|
||||||
|
"""
|
||||||
|
user_config = test_user
|
||||||
|
groupid = test_group
|
||||||
|
|
||||||
|
# Create the user
|
||||||
|
await nc_client.users.create_user(**user_config)
|
||||||
|
|
||||||
|
# Add user to group
|
||||||
|
await nc_client.users.add_user_to_group(user_config["userid"], groupid)
|
||||||
|
logger.debug(f"Added user {user_config['userid']} to group {groupid}")
|
||||||
|
|
||||||
|
yield (user_config, groupid)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ Multi-user OAuth tests for Nextcloud WebDAV file permissions.
|
|||||||
|
|
||||||
Tests verify that the MCP server respects Nextcloud file sharing permissions
|
Tests verify that the MCP server respects Nextcloud file sharing permissions
|
||||||
when accessed via OAuth authentication with different users.
|
when accessed via OAuth authentication with different users.
|
||||||
|
|
||||||
|
All operations (file creation, sharing, access) are performed through MCP tools
|
||||||
|
to ensure the MCP server properly supports multi-user scenarios.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -15,77 +18,48 @@ logger = logging.getLogger(__name__)
|
|||||||
pytestmark = [pytest.mark.integration, pytest.mark.oauth]
|
pytestmark = [pytest.mark.integration, pytest.mark.oauth]
|
||||||
|
|
||||||
|
|
||||||
async def create_share(nc_client, path: str, share_with: str, permissions: int = 1):
|
|
||||||
"""
|
|
||||||
Helper to create a file share using OCS Sharing API.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
nc_client: Admin NextcloudClient
|
|
||||||
path: Path to file/folder to share
|
|
||||||
share_with: Username to share with
|
|
||||||
permissions: Share permissions (1=read, 15=all, 19=read+write+share)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Share ID
|
|
||||||
"""
|
|
||||||
# Use the authenticated client's internal HTTP client
|
|
||||||
response = await nc_client._client.post(
|
|
||||||
"/ocs/v2.php/apps/files_sharing/api/v1/shares",
|
|
||||||
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
|
|
||||||
data={
|
|
||||||
"path": path,
|
|
||||||
"shareType": 0, # 0 = user share
|
|
||||||
"shareWith": share_with,
|
|
||||||
"permissions": permissions,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
share_id = data["ocs"]["data"]["id"]
|
|
||||||
logger.info(
|
|
||||||
f"Created share {share_id}: {path} -> {share_with} (permissions={permissions})"
|
|
||||||
)
|
|
||||||
return share_id
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_share(nc_client, share_id: int):
|
|
||||||
"""Helper to delete a file share."""
|
|
||||||
response = await nc_client._client.delete(
|
|
||||||
f"/ocs/v2.php/apps/files_sharing/api/v1/shares/{share_id}",
|
|
||||||
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
logger.info(f"Deleted share {share_id}")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_file_share_read_permissions(
|
async def test_file_share_read_permissions(
|
||||||
nc_client, alice_mcp_client, bob_mcp_client, diana_mcp_client
|
alice_mcp_client, bob_mcp_client, diana_mcp_client
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Test that shared files respect read permissions.
|
Test that shared files respect read permissions.
|
||||||
|
|
||||||
Scenario:
|
Scenario:
|
||||||
1. Admin creates a file as alice
|
1. Alice creates a file via MCP
|
||||||
2. Admin shares the file with bob (read-only)
|
2. Alice shares the file with Bob (read-only) via MCP
|
||||||
3. Bob can read the file via MCP tools
|
3. Bob can read the file via MCP tools
|
||||||
4. Diana cannot access the file (no share)
|
4. Diana cannot access the file (no share)
|
||||||
"""
|
"""
|
||||||
# Create a file as alice
|
|
||||||
file_path = "/alice_shared_file_read.txt"
|
file_path = "/alice_shared_file_read.txt"
|
||||||
file_content = b"This file is shared with Bob for reading only."
|
file_content = "This file is shared with Bob for reading only."
|
||||||
|
|
||||||
logger.info(f"Creating file as alice: {file_path}")
|
# Alice creates a file
|
||||||
# Note: We're using admin client to create file as alice
|
logger.info(f"Alice creating file: {file_path}")
|
||||||
# In a real scenario, we'd need to impersonate alice or use alice's OAuth client
|
result = await alice_mcp_client.call_tool(
|
||||||
await nc_client.webdav.write_file(file_path, file_content)
|
"nc_webdav_write_file",
|
||||||
|
arguments={"path": file_path, "content": file_content},
|
||||||
|
)
|
||||||
|
assert not result.isError, f"Alice failed to create file: {result.content}"
|
||||||
|
|
||||||
share_id = None
|
share_id = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Share the file with bob (read-only, permissions=1)
|
# Alice shares the file with bob (read-only, permissions=1)
|
||||||
logger.info("Sharing file with bob (read-only)...")
|
logger.info("Alice sharing file with bob (read-only)...")
|
||||||
share_id = await create_share(nc_client, file_path, "bob", permissions=1)
|
result = await alice_mcp_client.call_tool(
|
||||||
|
"nc_share_create",
|
||||||
|
arguments={
|
||||||
|
"path": file_path,
|
||||||
|
"share_with": "bob",
|
||||||
|
"share_type": 0,
|
||||||
|
"permissions": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert not result.isError, f"Alice failed to create share: {result.content}"
|
||||||
|
share_data = json.loads(result.content[0].text)
|
||||||
|
share_id = share_data["id"]
|
||||||
|
logger.info(f"Created share {share_id}")
|
||||||
|
|
||||||
# Test: Bob reads the file via MCP
|
# Test: Bob reads the file via MCP
|
||||||
logger.info("Bob attempting to read file via MCP...")
|
logger.info("Bob attempting to read file via MCP...")
|
||||||
@@ -100,6 +74,7 @@ async def test_file_share_read_permissions(
|
|||||||
f"Bob successfully read file: {response_data.get('content', '')[:50]}..."
|
f"Bob successfully read file: {response_data.get('content', '')[:50]}..."
|
||||||
)
|
)
|
||||||
assert "content" in response_data
|
assert "content" in response_data
|
||||||
|
assert file_content in response_data["content"]
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Bob could not read file: {result.content}")
|
logger.warning(f"Bob could not read file: {result.content}")
|
||||||
# This might fail if the share path is different for bob
|
# This might fail if the share path is different for bob
|
||||||
@@ -117,56 +92,86 @@ async def test_file_share_read_permissions(
|
|||||||
logger.warning("Diana unexpectedly could read unshared file")
|
logger.warning("Diana unexpectedly could read unshared file")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Cleanup
|
# Cleanup - Alice deletes the share and file
|
||||||
if share_id:
|
if share_id:
|
||||||
await delete_share(nc_client, share_id)
|
logger.info(f"Alice deleting share {share_id}")
|
||||||
logger.info(f"Deleting file {file_path}")
|
await alice_mcp_client.call_tool(
|
||||||
await nc_client.webdav.delete_resource(file_path)
|
"nc_share_delete", arguments={"share_id": share_id}
|
||||||
|
)
|
||||||
|
logger.info(f"Alice deleting file {file_path}")
|
||||||
|
await alice_mcp_client.call_tool(
|
||||||
|
"nc_webdav_delete_resource", arguments={"path": file_path}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_file_share_write_permissions(
|
async def test_file_share_write_permissions(
|
||||||
nc_client, alice_mcp_client, charlie_mcp_client, bob_mcp_client
|
alice_mcp_client, charlie_mcp_client, bob_mcp_client
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Test that shared files respect write permissions.
|
Test that shared files respect write permissions.
|
||||||
|
|
||||||
Scenario:
|
Scenario:
|
||||||
1. Admin creates a file as alice
|
1. Alice creates a file via MCP
|
||||||
2. Admin shares the file with charlie (edit permission)
|
2. Alice shares the file with Charlie (edit permission) via MCP
|
||||||
3. Admin shares the file with bob (read-only)
|
3. Alice shares the file with Bob (read-only) via MCP
|
||||||
4. Charlie can edit the file via MCP tools
|
4. Charlie can edit the file via MCP tools
|
||||||
5. Bob cannot edit the file
|
5. Bob cannot edit the file
|
||||||
"""
|
"""
|
||||||
# Create a file as alice
|
|
||||||
file_path = "/alice_shared_file_write.txt"
|
file_path = "/alice_shared_file_write.txt"
|
||||||
file_content = b"This file is shared with Charlie for editing."
|
file_content = "This file is shared with Charlie for editing."
|
||||||
|
|
||||||
logger.info(f"Creating file as alice: {file_path}")
|
logger.info(f"Alice creating file: {file_path}")
|
||||||
await nc_client.webdav.write_file(file_path, file_content)
|
result = await alice_mcp_client.call_tool(
|
||||||
|
"nc_webdav_write_file",
|
||||||
|
arguments={"path": file_path, "content": file_content},
|
||||||
|
)
|
||||||
|
assert not result.isError, f"Alice failed to create file: {result.content}"
|
||||||
|
|
||||||
charlie_share_id = None
|
charlie_share_id = None
|
||||||
bob_share_id = None
|
bob_share_id = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Share with charlie (read+write, permissions=3)
|
# Alice shares with Charlie (read+write, permissions=3)
|
||||||
logger.info("Sharing file with charlie (edit permission)...")
|
logger.info("Alice sharing file with Charlie (edit permission)...")
|
||||||
charlie_share_id = await create_share(
|
result = await alice_mcp_client.call_tool(
|
||||||
nc_client, file_path, "charlie", permissions=3
|
"nc_share_create",
|
||||||
|
arguments={
|
||||||
|
"path": file_path,
|
||||||
|
"share_with": "charlie",
|
||||||
|
"share_type": 0,
|
||||||
|
"permissions": 3,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
assert not result.isError, (
|
||||||
|
f"Alice failed to share with Charlie: {result.content}"
|
||||||
|
)
|
||||||
|
charlie_share_data = json.loads(result.content[0].text)
|
||||||
|
charlie_share_id = charlie_share_data["id"]
|
||||||
|
logger.info(f"Created share {charlie_share_id} for Charlie")
|
||||||
|
|
||||||
# Share with bob (read-only, permissions=1)
|
# Alice shares with Bob (read-only, permissions=1)
|
||||||
logger.info("Sharing file with bob (read-only)...")
|
logger.info("Alice sharing file with Bob (read-only)...")
|
||||||
bob_share_id = await create_share(nc_client, file_path, "bob", permissions=1)
|
result = await alice_mcp_client.call_tool(
|
||||||
|
"nc_share_create",
|
||||||
|
arguments={
|
||||||
|
"path": file_path,
|
||||||
|
"share_with": "bob",
|
||||||
|
"share_type": 0,
|
||||||
|
"permissions": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert not result.isError, f"Alice failed to share with Bob: {result.content}"
|
||||||
|
bob_share_data = json.loads(result.content[0].text)
|
||||||
|
bob_share_id = bob_share_data["id"]
|
||||||
|
logger.info(f"Created share {bob_share_id} for Bob")
|
||||||
|
|
||||||
# Test: Charlie can write to the file
|
# Test: Charlie can write to the file
|
||||||
logger.info("Charlie attempting to write to file via MCP...")
|
logger.info("Charlie attempting to write to file via MCP...")
|
||||||
updated_content = (
|
updated_content = f"{file_content}\nCharlie added this line."
|
||||||
b"This file is shared with Charlie for editing.\nCharlie added this line."
|
|
||||||
)
|
|
||||||
result = await charlie_mcp_client.call_tool(
|
result = await charlie_mcp_client.call_tool(
|
||||||
"nc_webdav_write_file",
|
"nc_webdav_write_file",
|
||||||
arguments={"path": file_path, "content": updated_content.decode("utf-8")},
|
arguments={"path": file_path, "content": updated_content},
|
||||||
)
|
)
|
||||||
|
|
||||||
if not result.isError:
|
if not result.isError:
|
||||||
@@ -188,46 +193,80 @@ async def test_file_share_write_permissions(
|
|||||||
logger.warning("Bob unexpectedly succeeded in writing (permissions issue?)")
|
logger.warning("Bob unexpectedly succeeded in writing (permissions issue?)")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Cleanup
|
# Cleanup - Alice deletes shares and file
|
||||||
if charlie_share_id:
|
if charlie_share_id:
|
||||||
await delete_share(nc_client, charlie_share_id)
|
logger.info(f"Alice deleting Charlie's share {charlie_share_id}")
|
||||||
|
await alice_mcp_client.call_tool(
|
||||||
|
"nc_share_delete", arguments={"share_id": charlie_share_id}
|
||||||
|
)
|
||||||
if bob_share_id:
|
if bob_share_id:
|
||||||
await delete_share(nc_client, bob_share_id)
|
logger.info(f"Alice deleting Bob's share {bob_share_id}")
|
||||||
logger.info(f"Deleting file {file_path}")
|
await alice_mcp_client.call_tool(
|
||||||
await nc_client.webdav.delete_resource(file_path)
|
"nc_share_delete", arguments={"share_id": bob_share_id}
|
||||||
|
)
|
||||||
|
logger.info(f"Alice deleting file {file_path}")
|
||||||
|
await alice_mcp_client.call_tool(
|
||||||
|
"nc_webdav_delete_resource", arguments={"path": file_path}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_file_list_permissions(nc_client, alice_mcp_client, bob_mcp_client):
|
async def test_file_list_permissions(alice_mcp_client, bob_mcp_client):
|
||||||
"""
|
"""
|
||||||
Test that file listing respects share permissions.
|
Test that file listing respects share permissions.
|
||||||
|
|
||||||
Scenario:
|
Scenario:
|
||||||
1. Admin creates alice's private file
|
1. Alice creates her private file via MCP
|
||||||
2. Admin creates bob's private file
|
2. Bob creates his private file via MCP
|
||||||
3. Admin creates a shared file
|
3. Alice creates a file and shares it with Bob via MCP
|
||||||
4. Alice can only list her own files + shared files
|
4. Alice can list her own files + shared files
|
||||||
5. Bob can only list his own files + shared files
|
5. Bob can list his own files + shared files from Alice
|
||||||
"""
|
"""
|
||||||
alice_file = "/alice_private_file.txt"
|
alice_file = "/alice_private_file.txt"
|
||||||
bob_file = "/bob_private_file.txt"
|
bob_file = "/bob_private_file.txt"
|
||||||
shared_file = "/shared_file.txt"
|
shared_file = "/alice_shared_with_bob.txt"
|
||||||
|
|
||||||
logger.info("Creating test files...")
|
# Alice creates her private file
|
||||||
await nc_client.webdav.write_file(alice_file, b"Alice's private file")
|
logger.info(f"Alice creating private file: {alice_file}")
|
||||||
await nc_client.webdav.write_file(bob_file, b"Bob's private file")
|
result = await alice_mcp_client.call_tool(
|
||||||
await nc_client.webdav.write_file(shared_file, b"Shared file content")
|
"nc_webdav_write_file",
|
||||||
|
arguments={"path": alice_file, "content": "Alice's private file"},
|
||||||
|
)
|
||||||
|
assert not result.isError, f"Alice failed to create file: {result.content}"
|
||||||
|
|
||||||
alice_share_id = None
|
# Bob creates his private file
|
||||||
bob_share_id = None
|
logger.info(f"Bob creating private file: {bob_file}")
|
||||||
|
result = await bob_mcp_client.call_tool(
|
||||||
|
"nc_webdav_write_file",
|
||||||
|
arguments={"path": bob_file, "content": "Bob's private file"},
|
||||||
|
)
|
||||||
|
assert not result.isError, f"Bob failed to create file: {result.content}"
|
||||||
|
|
||||||
|
# Alice creates a shared file
|
||||||
|
logger.info(f"Alice creating shared file: {shared_file}")
|
||||||
|
result = await alice_mcp_client.call_tool(
|
||||||
|
"nc_webdav_write_file",
|
||||||
|
arguments={"path": shared_file, "content": "Shared file content"},
|
||||||
|
)
|
||||||
|
assert not result.isError, f"Alice failed to create shared file: {result.content}"
|
||||||
|
|
||||||
|
share_id = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Share the shared file with both alice and bob
|
# Alice shares the file with Bob
|
||||||
logger.info("Sharing file with alice and bob...")
|
logger.info("Alice sharing file with Bob...")
|
||||||
alice_share_id = await create_share(
|
result = await alice_mcp_client.call_tool(
|
||||||
nc_client, shared_file, "alice", permissions=1
|
"nc_share_create",
|
||||||
|
arguments={
|
||||||
|
"path": shared_file,
|
||||||
|
"share_with": "bob",
|
||||||
|
"share_type": 0,
|
||||||
|
"permissions": 1,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
bob_share_id = await create_share(nc_client, shared_file, "bob", permissions=1)
|
assert not result.isError, f"Alice failed to create share: {result.content}"
|
||||||
|
share_data = json.loads(result.content[0].text)
|
||||||
|
share_id = share_data["id"]
|
||||||
|
|
||||||
# Test: Alice lists files in root
|
# Test: Alice lists files in root
|
||||||
logger.info("Alice listing files via MCP...")
|
logger.info("Alice listing files via MCP...")
|
||||||
@@ -237,14 +276,13 @@ async def test_file_list_permissions(nc_client, alice_mcp_client, bob_mcp_client
|
|||||||
|
|
||||||
if not result.isError:
|
if not result.isError:
|
||||||
response_data = json.loads(result.content[0].text)
|
response_data = json.loads(result.content[0].text)
|
||||||
# The response is directly a list, not wrapped in a dict
|
|
||||||
if not isinstance(response_data, list):
|
if not isinstance(response_data, list):
|
||||||
response_data = [response_data] if response_data else []
|
response_data = [response_data] if response_data else []
|
||||||
file_names = [f["name"] for f in response_data]
|
file_names = [f["name"] for f in response_data]
|
||||||
logger.info(f"Alice can see files: {file_names}")
|
logger.info(f"Alice can see files: {file_names}")
|
||||||
|
|
||||||
# Alice should see her own file and shared file, but not bob's
|
# Alice should see her own files
|
||||||
# Note: This depends on how Nextcloud handles file ownership
|
# Note: Exact assertions depend on test isolation
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Alice could not list files: {result.content}")
|
logger.warning(f"Alice could not list files: {result.content}")
|
||||||
|
|
||||||
@@ -256,56 +294,86 @@ async def test_file_list_permissions(nc_client, alice_mcp_client, bob_mcp_client
|
|||||||
|
|
||||||
if not result.isError:
|
if not result.isError:
|
||||||
response_data = json.loads(result.content[0].text)
|
response_data = json.loads(result.content[0].text)
|
||||||
# The response is directly a list, not wrapped in a dict
|
|
||||||
if not isinstance(response_data, list):
|
if not isinstance(response_data, list):
|
||||||
response_data = [response_data] if response_data else []
|
response_data = [response_data] if response_data else []
|
||||||
file_names = [f["name"] for f in response_data]
|
file_names = [f["name"] for f in response_data]
|
||||||
logger.info(f"Bob can see files: {file_names}")
|
logger.info(f"Bob can see files: {file_names}")
|
||||||
|
|
||||||
# Bob should see his own file and shared file, but not alice's
|
# Bob should see his own file, but not Alice's private file
|
||||||
|
# Bob may see shared files in his shared folder or via different path
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Bob could not list files: {result.content}")
|
logger.warning(f"Bob could not list files: {result.content}")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Cleanup
|
# Cleanup
|
||||||
if alice_share_id:
|
if share_id:
|
||||||
await delete_share(nc_client, alice_share_id)
|
logger.info(f"Alice deleting share {share_id}")
|
||||||
if bob_share_id:
|
await alice_mcp_client.call_tool(
|
||||||
await delete_share(nc_client, bob_share_id)
|
"nc_share_delete", arguments={"share_id": share_id}
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("Cleaning up test files...")
|
logger.info("Cleaning up Alice's files...")
|
||||||
await nc_client.webdav.delete_resource(alice_file)
|
await alice_mcp_client.call_tool(
|
||||||
await nc_client.webdav.delete_resource(bob_file)
|
"nc_webdav_delete_resource", arguments={"path": alice_file}
|
||||||
await nc_client.webdav.delete_resource(shared_file)
|
)
|
||||||
|
await alice_mcp_client.call_tool(
|
||||||
|
"nc_webdav_delete_resource", arguments={"path": shared_file}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Cleaning up Bob's files...")
|
||||||
|
await bob_mcp_client.call_tool(
|
||||||
|
"nc_webdav_delete_resource", arguments={"path": bob_file}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_folder_share_permissions(nc_client, alice_mcp_client, bob_mcp_client):
|
async def test_folder_share_permissions(alice_mcp_client, bob_mcp_client):
|
||||||
"""
|
"""
|
||||||
Test that folder sharing works correctly.
|
Test that folder sharing works correctly.
|
||||||
|
|
||||||
Scenario:
|
Scenario:
|
||||||
1. Admin creates a folder as alice
|
1. Alice creates a folder via MCP
|
||||||
2. Admin creates files in the folder
|
2. Alice creates files in the folder via MCP
|
||||||
3. Admin shares the folder with bob
|
3. Alice shares the folder with Bob via MCP
|
||||||
4. Bob can access files in the shared folder
|
4. Bob can access files in the shared folder via MCP
|
||||||
"""
|
"""
|
||||||
folder_path = "/alice_shared_folder"
|
folder_path = "/alice_shared_folder"
|
||||||
file_in_folder = f"{folder_path}/document.txt"
|
file_in_folder = f"{folder_path}/document.txt"
|
||||||
file_content = b"This is a document in alice's shared folder"
|
file_content = "This is a document in Alice's shared folder"
|
||||||
|
|
||||||
logger.info(f"Creating folder: {folder_path}")
|
# Alice creates folder
|
||||||
await nc_client.webdav.create_directory(folder_path)
|
logger.info(f"Alice creating folder: {folder_path}")
|
||||||
|
result = await alice_mcp_client.call_tool(
|
||||||
|
"nc_webdav_create_directory", arguments={"path": folder_path}
|
||||||
|
)
|
||||||
|
assert not result.isError, f"Alice failed to create folder: {result.content}"
|
||||||
|
|
||||||
logger.info(f"Creating file in folder: {file_in_folder}")
|
# Alice creates file in folder
|
||||||
await nc_client.webdav.write_file(file_in_folder, file_content)
|
logger.info(f"Alice creating file in folder: {file_in_folder}")
|
||||||
|
result = await alice_mcp_client.call_tool(
|
||||||
|
"nc_webdav_write_file",
|
||||||
|
arguments={"path": file_in_folder, "content": file_content},
|
||||||
|
)
|
||||||
|
assert not result.isError, f"Alice failed to create file: {result.content}"
|
||||||
|
|
||||||
share_id = None
|
share_id = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Share the folder with bob
|
# Alice shares the folder with Bob
|
||||||
logger.info("Sharing folder with bob...")
|
logger.info("Alice sharing folder with Bob...")
|
||||||
share_id = await create_share(nc_client, folder_path, "bob", permissions=1)
|
result = await alice_mcp_client.call_tool(
|
||||||
|
"nc_share_create",
|
||||||
|
arguments={
|
||||||
|
"path": folder_path,
|
||||||
|
"share_with": "bob",
|
||||||
|
"share_type": 0,
|
||||||
|
"permissions": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert not result.isError, f"Alice failed to create share: {result.content}"
|
||||||
|
share_data = json.loads(result.content[0].text)
|
||||||
|
share_id = share_data["id"]
|
||||||
|
logger.info(f"Created folder share {share_id}")
|
||||||
|
|
||||||
# Test: Bob lists the shared folder
|
# Test: Bob lists the shared folder
|
||||||
logger.info("Bob attempting to list shared folder via MCP...")
|
logger.info("Bob attempting to list shared folder via MCP...")
|
||||||
@@ -315,7 +383,6 @@ async def test_folder_share_permissions(nc_client, alice_mcp_client, bob_mcp_cli
|
|||||||
|
|
||||||
if not result.isError:
|
if not result.isError:
|
||||||
response_data = json.loads(result.content[0].text)
|
response_data = json.loads(result.content[0].text)
|
||||||
# The response is directly a list, not wrapped in a dict
|
|
||||||
if not isinstance(response_data, list):
|
if not isinstance(response_data, list):
|
||||||
response_data = [response_data] if response_data else []
|
response_data = [response_data] if response_data else []
|
||||||
logger.info(f"Bob can see {len(response_data)} files in shared folder")
|
logger.info(f"Bob can see {len(response_data)} files in shared folder")
|
||||||
@@ -338,15 +405,21 @@ async def test_folder_share_permissions(nc_client, alice_mcp_client, bob_mcp_cli
|
|||||||
response_data = json.loads(result.content[0].text)
|
response_data = json.loads(result.content[0].text)
|
||||||
logger.info("Bob successfully read file in shared folder")
|
logger.info("Bob successfully read file in shared folder")
|
||||||
assert "content" in response_data
|
assert "content" in response_data
|
||||||
|
assert file_content in response_data["content"]
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Bob could not read file in shared folder: {result.content}"
|
f"Bob could not read file in shared folder: {result.content}"
|
||||||
)
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Cleanup
|
# Cleanup - Alice deletes the share and folder
|
||||||
if share_id:
|
if share_id:
|
||||||
await delete_share(nc_client, share_id)
|
logger.info(f"Alice deleting share {share_id}")
|
||||||
|
await alice_mcp_client.call_tool(
|
||||||
|
"nc_share_delete", arguments={"share_id": share_id}
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("Cleaning up test folder...")
|
logger.info("Alice cleaning up test folder...")
|
||||||
await nc_client.webdav.delete_resource(folder_path)
|
await alice_mcp_client.call_tool(
|
||||||
|
"nc_webdav_delete_resource", arguments={"path": folder_path}
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,70 +3,53 @@ from nextcloud_mcp_server.client import NextcloudClient
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_and_delete_user(nc_client: NextcloudClient):
|
async def test_create_and_delete_user(nc_client: NextcloudClient, test_user):
|
||||||
userid = "testuser1"
|
"""Test creating a user and verifying deletion (cleanup by fixture)."""
|
||||||
password = "SecureTestPassword123!"
|
user_config = test_user
|
||||||
display_name = "Test User One"
|
|
||||||
email = "test1@example.com"
|
|
||||||
|
|
||||||
# Create user
|
# Create user
|
||||||
await nc_client.users.create_user(
|
await nc_client.users.create_user(**user_config)
|
||||||
userid=userid,
|
|
||||||
password=password,
|
|
||||||
display_name=display_name,
|
|
||||||
email=email,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verify user exists
|
# Verify user exists
|
||||||
users = await nc_client.users.search_users(search=userid)
|
users = await nc_client.users.search_users(search=user_config["userid"])
|
||||||
assert userid in users
|
assert user_config["userid"] in users
|
||||||
|
|
||||||
user_details = await nc_client.users.get_user_details(userid)
|
user_details = await nc_client.users.get_user_details(user_config["userid"])
|
||||||
assert user_details.id == userid
|
assert user_details.id == user_config["userid"]
|
||||||
assert user_details.displayname == display_name
|
assert user_details.displayname == user_config["display_name"]
|
||||||
assert user_details.email == email
|
assert user_details.email == user_config["email"]
|
||||||
|
|
||||||
# Delete user
|
# Test deletion explicitly as part of test functionality
|
||||||
await nc_client.users.delete_user(userid)
|
await nc_client.users.delete_user(user_config["userid"])
|
||||||
|
|
||||||
# Verify user is deleted
|
# Verify user is deleted
|
||||||
users = await nc_client.users.search_users(search=userid)
|
users = await nc_client.users.search_users(search=user_config["userid"])
|
||||||
assert userid not in users
|
assert user_config["userid"] not in users
|
||||||
|
# Note: Fixture cleanup will also try to delete but handle 404 gracefully
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_user_field(nc_client: NextcloudClient):
|
async def test_update_user_field(nc_client: NextcloudClient, test_user):
|
||||||
userid = "testuser2"
|
"""Test updating user fields."""
|
||||||
password = "SecureTestPassword123!"
|
user_config = test_user
|
||||||
display_name = "Test User Two"
|
|
||||||
email = "test2@example.com"
|
|
||||||
|
|
||||||
await nc_client.users.create_user(
|
await nc_client.users.create_user(**user_config)
|
||||||
userid=userid,
|
|
||||||
password=password,
|
|
||||||
display_name=display_name,
|
|
||||||
email=email,
|
|
||||||
)
|
|
||||||
|
|
||||||
new_email = "new.test2@example.com"
|
new_email = f"new.{user_config['email']}"
|
||||||
await nc_client.users.update_user_field(userid, "email", new_email)
|
await nc_client.users.update_user_field(user_config["userid"], "email", new_email)
|
||||||
|
|
||||||
user_details = await nc_client.users.get_user_details(userid)
|
user_details = await nc_client.users.get_user_details(user_config["userid"])
|
||||||
assert user_details.email == new_email
|
assert user_details.email == new_email
|
||||||
|
# Fixture will handle cleanup
|
||||||
await nc_client.users.delete_user(userid)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_user_groups(nc_client: NextcloudClient):
|
async def test_user_groups(nc_client: NextcloudClient, test_user_in_group):
|
||||||
userid = "testuser3"
|
"""Test adding and removing users from groups."""
|
||||||
password = "SecureTestPassword123!"
|
user_config, groupid = test_user_in_group
|
||||||
groupid = "testgroup"
|
userid = user_config["userid"]
|
||||||
|
|
||||||
await nc_client.users.create_user(userid=userid, password=password)
|
# Verify user is in group
|
||||||
|
|
||||||
# Add user to group
|
|
||||||
await nc_client.users.add_user_to_group(userid, groupid)
|
|
||||||
groups = await nc_client.users.get_user_groups(userid)
|
groups = await nc_client.users.get_user_groups(userid)
|
||||||
assert groupid in groups
|
assert groupid in groups
|
||||||
|
|
||||||
@@ -74,17 +57,17 @@ async def test_user_groups(nc_client: NextcloudClient):
|
|||||||
await nc_client.users.remove_user_from_group(userid, groupid)
|
await nc_client.users.remove_user_from_group(userid, groupid)
|
||||||
groups = await nc_client.users.get_user_groups(userid)
|
groups = await nc_client.users.get_user_groups(userid)
|
||||||
assert groupid not in groups
|
assert groupid not in groups
|
||||||
|
# Fixtures will handle cleanup
|
||||||
await nc_client.users.delete_user(userid)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_user_subadmins(nc_client: NextcloudClient):
|
async def test_user_subadmins(nc_client: NextcloudClient, test_user, test_group):
|
||||||
userid = "testuser4"
|
"""Test promoting and demoting subadmins."""
|
||||||
password = "SecureTestPassword123!"
|
user_config = test_user
|
||||||
groupid = "subadmingroup"
|
groupid = test_group
|
||||||
|
userid = user_config["userid"]
|
||||||
|
|
||||||
await nc_client.users.create_user(userid=userid, password=password)
|
await nc_client.users.create_user(**user_config)
|
||||||
|
|
||||||
# Promote to subadmin
|
# Promote to subadmin
|
||||||
await nc_client.users.promote_user_to_subadmin(userid, groupid)
|
await nc_client.users.promote_user_to_subadmin(userid, groupid)
|
||||||
@@ -95,16 +78,16 @@ async def test_user_subadmins(nc_client: NextcloudClient):
|
|||||||
await nc_client.users.demote_user_from_subadmin(userid, groupid)
|
await nc_client.users.demote_user_from_subadmin(userid, groupid)
|
||||||
subadmin_groups = await nc_client.users.get_user_subadmin_groups(userid)
|
subadmin_groups = await nc_client.users.get_user_subadmin_groups(userid)
|
||||||
assert groupid not in subadmin_groups
|
assert groupid not in subadmin_groups
|
||||||
|
# Fixtures will handle cleanup
|
||||||
await nc_client.users.delete_user(userid)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_disable_enable_user(nc_client: NextcloudClient):
|
async def test_disable_enable_user(nc_client: NextcloudClient, test_user):
|
||||||
userid = "testuser5"
|
"""Test disabling and enabling users."""
|
||||||
password = "SecureTestPassword123!"
|
user_config = test_user
|
||||||
|
userid = user_config["userid"]
|
||||||
|
|
||||||
await nc_client.users.create_user(userid=userid, password=password)
|
await nc_client.users.create_user(**user_config)
|
||||||
|
|
||||||
# Disable user
|
# Disable user
|
||||||
await nc_client.users.disable_user(userid)
|
await nc_client.users.disable_user(userid)
|
||||||
@@ -115,8 +98,7 @@ async def test_disable_enable_user(nc_client: NextcloudClient):
|
|||||||
await nc_client.users.enable_user(userid)
|
await nc_client.users.enable_user(userid)
|
||||||
user_details = await nc_client.users.get_user_details(userid)
|
user_details = await nc_client.users.get_user_details(userid)
|
||||||
assert user_details.enabled
|
assert user_details.enabled
|
||||||
|
# Fixture will handle cleanup
|
||||||
await nc_client.users.delete_user(userid)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user