Files
py-wikijs/tests/aio/test_async_users.py
Claude 2ea3936b5c Add comprehensive tests for Users API (70 tests total)
Tests include:
- User model validation (22 tests)
  - UserGroup, User, UserCreate, UserUpdate models
  - Field validation, email validation, password strength
  - CamelCase alias support

- Sync UsersEndpoint (24 tests)
  - List, get, create, update, delete operations
  - Search functionality
  - Pagination and filtering
  - Error handling and validation
  - Data normalization

- Async AsyncUsersEndpoint (24 tests)
  - Complete async coverage matching sync API
  - All CRUD operations tested
  - Validation and error handling

All 70 tests passing. Achieves >95% coverage for Users API code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 20:18:27 +00:00

660 lines
22 KiB
Python

"""Tests for async Users endpoint."""
from unittest.mock import AsyncMock, Mock
import pytest
from wikijs.aio.endpoints import AsyncUsersEndpoint
from wikijs.exceptions import APIError, ValidationError
from wikijs.models import User, UserCreate, UserUpdate
class TestAsyncUsersEndpoint:
"""Test AsyncUsersEndpoint class."""
@pytest.fixture
def client(self):
"""Create mock async client."""
mock_client = Mock()
mock_client.base_url = "https://wiki.example.com"
mock_client._request = AsyncMock()
return mock_client
@pytest.fixture
def endpoint(self, client):
"""Create AsyncUsersEndpoint instance."""
return AsyncUsersEndpoint(client)
@pytest.mark.asyncio
async def test_list_users_minimal(self, endpoint):
"""Test listing users with minimal parameters."""
# Mock response
mock_response = {
"data": {
"users": {
"list": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"providerKey": "local",
"isSystem": False,
"isActive": True,
"isVerified": True,
"location": None,
"jobTitle": None,
"timezone": None,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
"lastLoginAt": "2024-01-15T12:00:00Z",
}
]
}
}
}
endpoint._post = AsyncMock(return_value=mock_response)
# Call method
users = await endpoint.list()
# Verify
assert len(users) == 1
assert isinstance(users[0], User)
assert users[0].id == 1
assert users[0].name == "John Doe"
assert users[0].email == "john@example.com"
# Verify request
endpoint._post.assert_called_once()
@pytest.mark.asyncio
async def test_list_users_with_filters(self, endpoint):
"""Test listing users with filters."""
mock_response = {"data": {"users": {"list": []}}}
endpoint._post = AsyncMock(return_value=mock_response)
# Call with filters
users = await endpoint.list(
limit=10,
offset=5,
search="john",
order_by="email",
order_direction="DESC",
)
# Verify
assert users == []
endpoint._post.assert_called_once()
@pytest.mark.asyncio
async def test_list_users_pagination(self, endpoint):
"""Test client-side pagination."""
mock_response = {
"data": {
"users": {
"list": [
{
"id": i,
"name": f"User {i}",
"email": f"user{i}@example.com",
"providerKey": "local",
"isSystem": False,
"isActive": True,
"isVerified": True,
"location": None,
"jobTitle": None,
"timezone": None,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
"lastLoginAt": None,
}
for i in range(1, 11)
]
}
}
}
endpoint._post = AsyncMock(return_value=mock_response)
# Test offset
users = await endpoint.list(offset=5)
assert len(users) == 5
assert users[0].id == 6
# Test limit
endpoint._post.reset_mock()
endpoint._post.return_value = mock_response
users = await endpoint.list(limit=3)
assert len(users) == 3
# Test both
endpoint._post.reset_mock()
endpoint._post.return_value = mock_response
users = await endpoint.list(offset=2, limit=3)
assert len(users) == 3
assert users[0].id == 3
@pytest.mark.asyncio
async def test_list_users_validation_errors(self, endpoint):
"""Test validation errors in list."""
# Invalid limit
with pytest.raises(ValidationError) as exc_info:
await endpoint.list(limit=0)
assert "greater than 0" in str(exc_info.value)
# Invalid offset
with pytest.raises(ValidationError) as exc_info:
await endpoint.list(offset=-1)
assert "non-negative" in str(exc_info.value)
# Invalid order_by
with pytest.raises(ValidationError) as exc_info:
await endpoint.list(order_by="invalid")
assert "must be one of" in str(exc_info.value)
# Invalid order_direction
with pytest.raises(ValidationError) as exc_info:
await endpoint.list(order_direction="INVALID")
assert "must be ASC or DESC" in str(exc_info.value)
@pytest.mark.asyncio
async def test_list_users_api_error(self, endpoint):
"""Test API error handling in list."""
mock_response = {"errors": [{"message": "GraphQL error"}]}
endpoint._post = AsyncMock(return_value=mock_response)
with pytest.raises(APIError) as exc_info:
await endpoint.list()
assert "GraphQL errors" in str(exc_info.value)
@pytest.mark.asyncio
async def test_get_user(self, endpoint):
"""Test getting a single user."""
mock_response = {
"data": {
"users": {
"single": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"providerKey": "local",
"isSystem": False,
"isActive": True,
"isVerified": True,
"location": "New York",
"jobTitle": "Developer",
"timezone": "America/New_York",
"groups": [
{"id": 1, "name": "Administrators"},
{"id": 2, "name": "Editors"},
],
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
"lastLoginAt": "2024-01-15T12:00:00Z",
}
}
}
}
endpoint._post = AsyncMock(return_value=mock_response)
# Call method
user = await endpoint.get(1)
# Verify
assert isinstance(user, User)
assert user.id == 1
assert user.name == "John Doe"
assert user.email == "john@example.com"
assert user.location == "New York"
assert user.job_title == "Developer"
assert len(user.groups) == 2
assert user.groups[0].name == "Administrators"
@pytest.mark.asyncio
async def test_get_user_not_found(self, endpoint):
"""Test getting non-existent user."""
mock_response = {"data": {"users": {"single": None}}}
endpoint._post = AsyncMock(return_value=mock_response)
with pytest.raises(APIError) as exc_info:
await endpoint.get(999)
assert "not found" in str(exc_info.value)
@pytest.mark.asyncio
async def test_get_user_validation_error(self, endpoint):
"""Test validation error in get."""
with pytest.raises(ValidationError) as exc_info:
await endpoint.get(0)
assert "positive integer" in str(exc_info.value)
with pytest.raises(ValidationError) as exc_info:
await endpoint.get(-1)
assert "positive integer" in str(exc_info.value)
with pytest.raises(ValidationError) as exc_info:
await endpoint.get("not-an-int")
assert "positive integer" in str(exc_info.value)
@pytest.mark.asyncio
async def test_create_user_from_model(self, endpoint):
"""Test creating user from UserCreate model."""
user_data = UserCreate(
email="new@example.com",
name="New User",
password_raw="secret123",
groups=[1, 2],
)
mock_response = {
"data": {
"users": {
"create": {
"responseResult": {
"succeeded": True,
"errorCode": 0,
"slug": "ok",
"message": "User created successfully",
},
"user": {
"id": 2,
"name": "New User",
"email": "new@example.com",
"providerKey": "local",
"isSystem": False,
"isActive": True,
"isVerified": False,
"location": None,
"jobTitle": None,
"timezone": None,
"createdAt": "2024-01-20T00:00:00Z",
"updatedAt": "2024-01-20T00:00:00Z",
},
}
}
}
}
endpoint._post = AsyncMock(return_value=mock_response)
# Call method
user = await endpoint.create(user_data)
# Verify
assert isinstance(user, User)
assert user.id == 2
assert user.name == "New User"
assert user.email == "new@example.com"
# Verify request
endpoint._post.assert_called_once()
call_args = endpoint._post.call_args
assert call_args[1]["json_data"]["variables"]["email"] == "new@example.com"
assert call_args[1]["json_data"]["variables"]["groups"] == [1, 2]
@pytest.mark.asyncio
async def test_create_user_from_dict(self, endpoint):
"""Test creating user from dictionary."""
user_data = {
"email": "new@example.com",
"name": "New User",
"password_raw": "secret123",
}
mock_response = {
"data": {
"users": {
"create": {
"responseResult": {"succeeded": True},
"user": {
"id": 2,
"name": "New User",
"email": "new@example.com",
"providerKey": "local",
"isSystem": False,
"isActive": True,
"isVerified": False,
"location": None,
"jobTitle": None,
"timezone": None,
"createdAt": "2024-01-20T00:00:00Z",
"updatedAt": "2024-01-20T00:00:00Z",
},
}
}
}
}
endpoint._post = AsyncMock(return_value=mock_response)
# Call method
user = await endpoint.create(user_data)
# Verify
assert isinstance(user, User)
assert user.name == "New User"
@pytest.mark.asyncio
async def test_create_user_api_failure(self, endpoint):
"""Test API failure in create."""
user_data = UserCreate(
email="new@example.com", name="New User", password_raw="secret123"
)
mock_response = {
"data": {
"users": {
"create": {
"responseResult": {
"succeeded": False,
"message": "Email already exists",
}
}
}
}
}
endpoint._post = AsyncMock(return_value=mock_response)
with pytest.raises(APIError) as exc_info:
await endpoint.create(user_data)
assert "Email already exists" in str(exc_info.value)
@pytest.mark.asyncio
async def test_create_user_validation_error(self, endpoint):
"""Test validation error in create."""
# Invalid user data
with pytest.raises(ValidationError):
await endpoint.create({"email": "invalid"})
# Wrong type
with pytest.raises(ValidationError):
await endpoint.create("not-a-dict-or-model")
@pytest.mark.asyncio
async def test_update_user_from_model(self, endpoint):
"""Test updating user from UserUpdate model."""
user_data = UserUpdate(name="Updated Name", location="San Francisco")
mock_response = {
"data": {
"users": {
"update": {
"responseResult": {"succeeded": True},
"user": {
"id": 1,
"name": "Updated Name",
"email": "john@example.com",
"providerKey": "local",
"isSystem": False,
"isActive": True,
"isVerified": True,
"location": "San Francisco",
"jobTitle": None,
"timezone": None,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-20T00:00:00Z",
},
}
}
}
}
endpoint._post = AsyncMock(return_value=mock_response)
# Call method
user = await endpoint.update(1, user_data)
# Verify
assert isinstance(user, User)
assert user.name == "Updated Name"
assert user.location == "San Francisco"
# Verify only non-None fields were sent
call_args = endpoint._post.call_args
variables = call_args[1]["json_data"]["variables"]
assert "name" in variables
assert "location" in variables
assert "email" not in variables # Not updated
@pytest.mark.asyncio
async def test_update_user_from_dict(self, endpoint):
"""Test updating user from dictionary."""
user_data = {"name": "Updated Name"}
mock_response = {
"data": {
"users": {
"update": {
"responseResult": {"succeeded": True},
"user": {
"id": 1,
"name": "Updated Name",
"email": "john@example.com",
"providerKey": "local",
"isSystem": False,
"isActive": True,
"isVerified": True,
"location": None,
"jobTitle": None,
"timezone": None,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-20T00:00:00Z",
},
}
}
}
}
endpoint._post = AsyncMock(return_value=mock_response)
# Call method
user = await endpoint.update(1, user_data)
# Verify
assert user.name == "Updated Name"
@pytest.mark.asyncio
async def test_update_user_api_failure(self, endpoint):
"""Test API failure in update."""
user_data = UserUpdate(name="Updated Name")
mock_response = {
"data": {
"users": {
"update": {
"responseResult": {
"succeeded": False,
"message": "User not found",
}
}
}
}
}
endpoint._post = AsyncMock(return_value=mock_response)
with pytest.raises(APIError) as exc_info:
await endpoint.update(999, user_data)
assert "User not found" in str(exc_info.value)
@pytest.mark.asyncio
async def test_update_user_validation_error(self, endpoint):
"""Test validation error in update."""
# Invalid user ID
with pytest.raises(ValidationError):
await endpoint.update(0, UserUpdate(name="Test"))
# Invalid user data
with pytest.raises(ValidationError):
await endpoint.update(1, {"name": ""}) # Empty name
# Wrong type
with pytest.raises(ValidationError):
await endpoint.update(1, "not-a-dict-or-model")
@pytest.mark.asyncio
async def test_delete_user(self, endpoint):
"""Test deleting a user."""
mock_response = {
"data": {
"users": {
"delete": {
"responseResult": {
"succeeded": True,
"message": "User deleted successfully",
}
}
}
}
}
endpoint._post = AsyncMock(return_value=mock_response)
# Call method
result = await endpoint.delete(1)
# Verify
assert result is True
endpoint._post.assert_called_once()
@pytest.mark.asyncio
async def test_delete_user_api_failure(self, endpoint):
"""Test API failure in delete."""
mock_response = {
"data": {
"users": {
"delete": {
"responseResult": {
"succeeded": False,
"message": "Cannot delete system user",
}
}
}
}
}
endpoint._post = AsyncMock(return_value=mock_response)
with pytest.raises(APIError) as exc_info:
await endpoint.delete(1)
assert "Cannot delete system user" in str(exc_info.value)
@pytest.mark.asyncio
async def test_delete_user_validation_error(self, endpoint):
"""Test validation error in delete."""
with pytest.raises(ValidationError):
await endpoint.delete(0)
with pytest.raises(ValidationError):
await endpoint.delete(-1)
with pytest.raises(ValidationError):
await endpoint.delete("not-an-int")
@pytest.mark.asyncio
async def test_search_users(self, endpoint):
"""Test searching users."""
mock_response = {
"data": {
"users": {
"list": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"providerKey": "local",
"isSystem": False,
"isActive": True,
"isVerified": True,
"location": None,
"jobTitle": None,
"timezone": None,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
"lastLoginAt": None,
}
]
}
}
}
endpoint._post = AsyncMock(return_value=mock_response)
# Call method
users = await endpoint.search("john")
# Verify
assert len(users) == 1
assert users[0].name == "John Doe"
@pytest.mark.asyncio
async def test_search_users_with_limit(self, endpoint):
"""Test searching users with limit."""
mock_response = {"data": {"users": {"list": []}}}
endpoint._post = AsyncMock(return_value=mock_response)
users = await endpoint.search("test", limit=5)
assert users == []
@pytest.mark.asyncio
async def test_search_users_validation_error(self, endpoint):
"""Test validation error in search."""
# Empty query
with pytest.raises(ValidationError):
await endpoint.search("")
# Non-string query
with pytest.raises(ValidationError):
await endpoint.search(123)
# Invalid limit
with pytest.raises(ValidationError):
await endpoint.search("test", limit=0)
@pytest.mark.asyncio
async def test_normalize_user_data(self, endpoint):
"""Test user data normalization."""
api_data = {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"providerKey": "local",
"isSystem": False,
"isActive": True,
"isVerified": True,
"location": "New York",
"jobTitle": "Developer",
"timezone": "America/New_York",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
"lastLoginAt": "2024-01-15T12:00:00Z",
"groups": [{"id": 1, "name": "Administrators"}],
}
normalized = endpoint._normalize_user_data(api_data)
# Verify snake_case conversion
assert normalized["id"] == 1
assert normalized["name"] == "John Doe"
assert normalized["email"] == "john@example.com"
assert normalized["provider_key"] == "local"
assert normalized["is_system"] is False
assert normalized["is_active"] is True
assert normalized["is_verified"] is True
assert normalized["job_title"] == "Developer"
assert normalized["last_login_at"] == "2024-01-15T12:00:00Z"
assert len(normalized["groups"]) == 1
assert normalized["groups"][0]["name"] == "Administrators"
@pytest.mark.asyncio
async def test_normalize_user_data_no_groups(self, endpoint):
"""Test normalization with no groups."""
api_data = {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"providerKey": "local",
"isSystem": False,
"isActive": True,
"isVerified": True,
"location": None,
"jobTitle": None,
"timezone": None,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
"lastLoginAt": None,
}
normalized = endpoint._normalize_user_data(api_data)
assert normalized["groups"] == []