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>
This commit is contained in:
Claude
2025-10-22 20:18:27 +00:00
parent db4f284cc7
commit 2ea3936b5c
3 changed files with 1702 additions and 0 deletions

View File

@@ -0,0 +1,659 @@
"""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"] == []

View File

@@ -0,0 +1,640 @@
"""Tests for Users endpoint."""
from unittest.mock import Mock, patch
import pytest
from wikijs.endpoints import UsersEndpoint
from wikijs.exceptions import APIError, ValidationError
from wikijs.models import User, UserCreate, UserUpdate
class TestUsersEndpoint:
"""Test UsersEndpoint class."""
@pytest.fixture
def client(self):
"""Create mock client."""
mock_client = Mock()
mock_client.base_url = "https://wiki.example.com"
mock_client._request = Mock()
return mock_client
@pytest.fixture
def endpoint(self, client):
"""Create UsersEndpoint instance."""
return UsersEndpoint(client)
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 = Mock(return_value=mock_response)
# Call method
users = 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()
call_args = endpoint._post.call_args
assert "/graphql" in str(call_args)
def test_list_users_with_filters(self, endpoint):
"""Test listing users with filters."""
mock_response = {"data": {"users": {"list": []}}}
endpoint._post = Mock(return_value=mock_response)
# Call with filters
users = endpoint.list(
limit=10,
offset=5,
search="john",
order_by="email",
order_direction="DESC",
)
# Verify
assert users == []
endpoint._post.assert_called_once()
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 = Mock(return_value=mock_response)
# Test offset
users = 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 = endpoint.list(limit=3)
assert len(users) == 3
# Test both
endpoint._post.reset_mock()
endpoint._post.return_value = mock_response
users = endpoint.list(offset=2, limit=3)
assert len(users) == 3
assert users[0].id == 3
def test_list_users_validation_errors(self, endpoint):
"""Test validation errors in list."""
# Invalid limit
with pytest.raises(ValidationError) as exc_info:
endpoint.list(limit=0)
assert "greater than 0" in str(exc_info.value)
# Invalid offset
with pytest.raises(ValidationError) as exc_info:
endpoint.list(offset=-1)
assert "non-negative" in str(exc_info.value)
# Invalid order_by
with pytest.raises(ValidationError) as exc_info:
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:
endpoint.list(order_direction="INVALID")
assert "must be ASC or DESC" in str(exc_info.value)
def test_list_users_api_error(self, endpoint):
"""Test API error handling in list."""
mock_response = {"errors": [{"message": "GraphQL error"}]}
endpoint._post = Mock(return_value=mock_response)
with pytest.raises(APIError) as exc_info:
endpoint.list()
assert "GraphQL errors" in str(exc_info.value)
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 = Mock(return_value=mock_response)
# Call method
user = 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"
# Verify request
endpoint._post.assert_called_once()
def test_get_user_not_found(self, endpoint):
"""Test getting non-existent user."""
mock_response = {"data": {"users": {"single": None}}}
endpoint._post = Mock(return_value=mock_response)
with pytest.raises(APIError) as exc_info:
endpoint.get(999)
assert "not found" in str(exc_info.value)
def test_get_user_validation_error(self, endpoint):
"""Test validation error in get."""
with pytest.raises(ValidationError) as exc_info:
endpoint.get(0)
assert "positive integer" in str(exc_info.value)
with pytest.raises(ValidationError) as exc_info:
endpoint.get(-1)
assert "positive integer" in str(exc_info.value)
with pytest.raises(ValidationError) as exc_info:
endpoint.get("not-an-int")
assert "positive integer" in str(exc_info.value)
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 = Mock(return_value=mock_response)
# Call method
user = 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]
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 = Mock(return_value=mock_response)
# Call method
user = endpoint.create(user_data)
# Verify
assert isinstance(user, User)
assert user.name == "New User"
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 = Mock(return_value=mock_response)
with pytest.raises(APIError) as exc_info:
endpoint.create(user_data)
assert "Email already exists" in str(exc_info.value)
def test_create_user_validation_error(self, endpoint):
"""Test validation error in create."""
# Invalid user data
with pytest.raises(ValidationError):
endpoint.create({"email": "invalid"})
# Wrong type
with pytest.raises(ValidationError):
endpoint.create("not-a-dict-or-model")
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 = Mock(return_value=mock_response)
# Call method
user = 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
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 = Mock(return_value=mock_response)
# Call method
user = endpoint.update(1, user_data)
# Verify
assert user.name == "Updated Name"
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 = Mock(return_value=mock_response)
with pytest.raises(APIError) as exc_info:
endpoint.update(999, user_data)
assert "User not found" in str(exc_info.value)
def test_update_user_validation_error(self, endpoint):
"""Test validation error in update."""
# Invalid user ID
with pytest.raises(ValidationError):
endpoint.update(0, UserUpdate(name="Test"))
# Invalid user data
with pytest.raises(ValidationError):
endpoint.update(1, {"name": ""}) # Empty name
# Wrong type
with pytest.raises(ValidationError):
endpoint.update(1, "not-a-dict-or-model")
def test_delete_user(self, endpoint):
"""Test deleting a user."""
mock_response = {
"data": {
"users": {
"delete": {
"responseResult": {
"succeeded": True,
"message": "User deleted successfully",
}
}
}
}
}
endpoint._post = Mock(return_value=mock_response)
# Call method
result = endpoint.delete(1)
# Verify
assert result is True
endpoint._post.assert_called_once()
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 = Mock(return_value=mock_response)
with pytest.raises(APIError) as exc_info:
endpoint.delete(1)
assert "Cannot delete system user" in str(exc_info.value)
def test_delete_user_validation_error(self, endpoint):
"""Test validation error in delete."""
with pytest.raises(ValidationError):
endpoint.delete(0)
with pytest.raises(ValidationError):
endpoint.delete(-1)
with pytest.raises(ValidationError):
endpoint.delete("not-an-int")
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 = Mock(return_value=mock_response)
# Call method
users = endpoint.search("john")
# Verify
assert len(users) == 1
assert users[0].name == "John Doe"
def test_search_users_with_limit(self, endpoint):
"""Test searching users with limit."""
mock_response = {"data": {"users": {"list": []}}}
endpoint._post = Mock(return_value=mock_response)
users = endpoint.search("test", limit=5)
assert users == []
def test_search_users_validation_error(self, endpoint):
"""Test validation error in search."""
# Empty query
with pytest.raises(ValidationError):
endpoint.search("")
# Non-string query
with pytest.raises(ValidationError):
endpoint.search(123)
# Invalid limit
with pytest.raises(ValidationError):
endpoint.search("test", limit=0)
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"
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"] == []

403
tests/models/test_user.py Normal file
View File

@@ -0,0 +1,403 @@
"""Tests for User data models."""
import pytest
from pydantic import ValidationError
from wikijs.models import User, UserCreate, UserGroup, UserUpdate
class TestUserGroup:
"""Test UserGroup model."""
def test_user_group_creation(self):
"""Test creating a valid user group."""
group = UserGroup(id=1, name="Administrators")
assert group.id == 1
assert group.name == "Administrators"
def test_user_group_required_fields(self):
"""Test that required fields are enforced."""
with pytest.raises(ValidationError) as exc_info:
UserGroup(id=1)
assert "name" in str(exc_info.value)
with pytest.raises(ValidationError) as exc_info:
UserGroup(name="Administrators")
assert "id" in str(exc_info.value)
class TestUser:
"""Test User model."""
def test_user_creation_minimal(self):
"""Test creating a user with minimal required fields."""
user = User(
id=1,
name="John Doe",
email="john@example.com",
created_at="2024-01-01T00:00:00Z",
updated_at="2024-01-01T00:00:00Z",
)
assert user.id == 1
assert user.name == "John Doe"
assert user.email == "john@example.com"
assert user.is_active is True
assert user.is_system is False
assert user.is_verified is False
assert user.groups == []
def test_user_creation_full(self):
"""Test creating a user with all fields."""
groups = [
UserGroup(id=1, name="Administrators"),
UserGroup(id=2, name="Editors"),
]
user = User(
id=1,
name="John Doe",
email="john@example.com",
provider_key="local",
is_system=False,
is_active=True,
is_verified=True,
location="New York",
job_title="Senior Developer",
timezone="America/New_York",
groups=groups,
created_at="2024-01-01T00:00:00Z",
updated_at="2024-01-01T00:00:00Z",
last_login_at="2024-01-15T12:00:00Z",
)
assert user.id == 1
assert user.name == "John Doe"
assert user.email == "john@example.com"
assert user.provider_key == "local"
assert user.is_system is False
assert user.is_active is True
assert user.is_verified is True
assert user.location == "New York"
assert user.job_title == "Senior Developer"
assert user.timezone == "America/New_York"
assert len(user.groups) == 2
assert user.groups[0].name == "Administrators"
assert user.last_login_at == "2024-01-15T12:00:00Z"
def test_user_camel_case_alias(self):
"""Test that camelCase aliases work."""
user = User(
id=1,
name="John Doe",
email="john@example.com",
providerKey="local",
isSystem=False,
isActive=True,
isVerified=True,
jobTitle="Developer",
createdAt="2024-01-01T00:00:00Z",
updatedAt="2024-01-01T00:00:00Z",
lastLoginAt="2024-01-15T12:00:00Z",
)
assert user.provider_key == "local"
assert user.is_system is False
assert user.is_active is True
assert user.is_verified is True
assert user.job_title == "Developer"
assert user.last_login_at == "2024-01-15T12:00:00Z"
def test_user_required_fields(self):
"""Test that required fields are enforced."""
with pytest.raises(ValidationError) as exc_info:
User(name="John Doe", email="john@example.com")
assert "id" in str(exc_info.value)
with pytest.raises(ValidationError) as exc_info:
User(id=1, email="john@example.com")
assert "name" in str(exc_info.value)
with pytest.raises(ValidationError) as exc_info:
User(id=1, name="John Doe")
assert "email" in str(exc_info.value)
def test_user_email_validation(self):
"""Test email validation."""
# Valid email
user = User(
id=1,
name="John Doe",
email="john@example.com",
created_at="2024-01-01T00:00:00Z",
updated_at="2024-01-01T00:00:00Z",
)
assert user.email == "john@example.com"
# Invalid email
with pytest.raises(ValidationError) as exc_info:
User(
id=1,
name="John Doe",
email="not-an-email",
created_at="2024-01-01T00:00:00Z",
updated_at="2024-01-01T00:00:00Z",
)
assert "email" in str(exc_info.value).lower()
def test_user_name_validation(self):
"""Test name validation."""
# Too short
with pytest.raises(ValidationError) as exc_info:
User(
id=1,
name="J",
email="john@example.com",
created_at="2024-01-01T00:00:00Z",
updated_at="2024-01-01T00:00:00Z",
)
assert "at least 2 characters" in str(exc_info.value)
# Too long
with pytest.raises(ValidationError) as exc_info:
User(
id=1,
name="x" * 256,
email="john@example.com",
created_at="2024-01-01T00:00:00Z",
updated_at="2024-01-01T00:00:00Z",
)
assert "cannot exceed 255 characters" in str(exc_info.value)
# Empty
with pytest.raises(ValidationError) as exc_info:
User(
id=1,
name="",
email="john@example.com",
created_at="2024-01-01T00:00:00Z",
updated_at="2024-01-01T00:00:00Z",
)
assert "cannot be empty" in str(exc_info.value)
# Whitespace trimming
user = User(
id=1,
name=" John Doe ",
email="john@example.com",
created_at="2024-01-01T00:00:00Z",
updated_at="2024-01-01T00:00:00Z",
)
assert user.name == "John Doe"
class TestUserCreate:
"""Test UserCreate model."""
def test_user_create_minimal(self):
"""Test creating user with minimal required fields."""
user_data = UserCreate(
email="john@example.com", name="John Doe", password_raw="secret123"
)
assert user_data.email == "john@example.com"
assert user_data.name == "John Doe"
assert user_data.password_raw == "secret123"
assert user_data.provider_key == "local"
assert user_data.groups == []
assert user_data.must_change_password is False
assert user_data.send_welcome_email is True
def test_user_create_full(self):
"""Test creating user with all fields."""
user_data = UserCreate(
email="john@example.com",
name="John Doe",
password_raw="secret123",
provider_key="ldap",
groups=[1, 2, 3],
must_change_password=True,
send_welcome_email=False,
location="New York",
job_title="Developer",
timezone="America/New_York",
)
assert user_data.email == "john@example.com"
assert user_data.name == "John Doe"
assert user_data.password_raw == "secret123"
assert user_data.provider_key == "ldap"
assert user_data.groups == [1, 2, 3]
assert user_data.must_change_password is True
assert user_data.send_welcome_email is False
assert user_data.location == "New York"
assert user_data.job_title == "Developer"
assert user_data.timezone == "America/New_York"
def test_user_create_camel_case_alias(self):
"""Test that camelCase aliases work."""
user_data = UserCreate(
email="john@example.com",
name="John Doe",
passwordRaw="secret123",
providerKey="ldap",
mustChangePassword=True,
sendWelcomeEmail=False,
jobTitle="Developer",
)
assert user_data.password_raw == "secret123"
assert user_data.provider_key == "ldap"
assert user_data.must_change_password is True
assert user_data.send_welcome_email is False
assert user_data.job_title == "Developer"
def test_user_create_required_fields(self):
"""Test that required fields are enforced."""
with pytest.raises(ValidationError) as exc_info:
UserCreate(name="John Doe", password_raw="secret123")
assert "email" in str(exc_info.value)
with pytest.raises(ValidationError) as exc_info:
UserCreate(email="john@example.com", password_raw="secret123")
assert "name" in str(exc_info.value)
with pytest.raises(ValidationError) as exc_info:
UserCreate(email="john@example.com", name="John Doe")
# Pydantic uses the field alias in error messages
assert "passwordRaw" in str(exc_info.value) or "password_raw" in str(
exc_info.value
)
def test_user_create_email_validation(self):
"""Test email validation."""
with pytest.raises(ValidationError) as exc_info:
UserCreate(email="not-an-email", name="John Doe", password_raw="secret123")
assert "email" in str(exc_info.value).lower()
def test_user_create_name_validation(self):
"""Test name validation."""
# Too short
with pytest.raises(ValidationError) as exc_info:
UserCreate(email="john@example.com", name="J", password_raw="secret123")
assert "at least 2 characters" in str(exc_info.value)
# Too long
with pytest.raises(ValidationError) as exc_info:
UserCreate(
email="john@example.com", name="x" * 256, password_raw="secret123"
)
assert "cannot exceed 255 characters" in str(exc_info.value)
# Empty
with pytest.raises(ValidationError) as exc_info:
UserCreate(email="john@example.com", name="", password_raw="secret123")
assert "cannot be empty" in str(exc_info.value)
def test_user_create_password_validation(self):
"""Test password validation."""
# Too short
with pytest.raises(ValidationError) as exc_info:
UserCreate(email="john@example.com", name="John Doe", password_raw="123")
assert "at least 6 characters" in str(exc_info.value)
# Too long
with pytest.raises(ValidationError) as exc_info:
UserCreate(
email="john@example.com", name="John Doe", password_raw="x" * 256
)
assert "cannot exceed 255 characters" in str(exc_info.value)
# Empty
with pytest.raises(ValidationError) as exc_info:
UserCreate(email="john@example.com", name="John Doe", password_raw="")
assert "cannot be empty" in str(exc_info.value)
class TestUserUpdate:
"""Test UserUpdate model."""
def test_user_update_all_none(self):
"""Test creating empty update."""
user_data = UserUpdate()
assert user_data.name is None
assert user_data.email is None
assert user_data.password_raw is None
assert user_data.location is None
assert user_data.job_title is None
assert user_data.timezone is None
assert user_data.groups is None
assert user_data.is_active is None
assert user_data.is_verified is None
def test_user_update_partial(self):
"""Test partial updates."""
user_data = UserUpdate(name="Jane Doe", email="jane@example.com")
assert user_data.name == "Jane Doe"
assert user_data.email == "jane@example.com"
assert user_data.password_raw is None
assert user_data.location is None
def test_user_update_full(self):
"""Test full update."""
user_data = UserUpdate(
name="Jane Doe",
email="jane@example.com",
password_raw="newsecret123",
location="San Francisco",
job_title="Senior Developer",
timezone="America/Los_Angeles",
groups=[1, 2],
is_active=False,
is_verified=True,
)
assert user_data.name == "Jane Doe"
assert user_data.email == "jane@example.com"
assert user_data.password_raw == "newsecret123"
assert user_data.location == "San Francisco"
assert user_data.job_title == "Senior Developer"
assert user_data.timezone == "America/Los_Angeles"
assert user_data.groups == [1, 2]
assert user_data.is_active is False
assert user_data.is_verified is True
def test_user_update_camel_case_alias(self):
"""Test that camelCase aliases work."""
user_data = UserUpdate(
passwordRaw="newsecret123",
jobTitle="Senior Developer",
isActive=False,
isVerified=True,
)
assert user_data.password_raw == "newsecret123"
assert user_data.job_title == "Senior Developer"
assert user_data.is_active is False
assert user_data.is_verified is True
def test_user_update_email_validation(self):
"""Test email validation."""
with pytest.raises(ValidationError) as exc_info:
UserUpdate(email="not-an-email")
assert "email" in str(exc_info.value).lower()
def test_user_update_name_validation(self):
"""Test name validation."""
# Too short
with pytest.raises(ValidationError) as exc_info:
UserUpdate(name="J")
assert "at least 2 characters" in str(exc_info.value)
# Too long
with pytest.raises(ValidationError) as exc_info:
UserUpdate(name="x" * 256)
assert "cannot exceed 255 characters" in str(exc_info.value)
# Empty
with pytest.raises(ValidationError) as exc_info:
UserUpdate(name="")
assert "cannot be empty" in str(exc_info.value)
def test_user_update_password_validation(self):
"""Test password validation."""
# Too short
with pytest.raises(ValidationError) as exc_info:
UserUpdate(password_raw="123")
assert "at least 6 characters" in str(exc_info.value)
# Too long
with pytest.raises(ValidationError) as exc_info:
UserUpdate(password_raw="x" * 256)
assert "cannot exceed 255 characters" in str(exc_info.value)