diff --git a/tests/aio/test_async_users.py b/tests/aio/test_async_users.py new file mode 100644 index 0000000..62fe78b --- /dev/null +++ b/tests/aio/test_async_users.py @@ -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"] == [] diff --git a/tests/endpoints/test_users.py b/tests/endpoints/test_users.py new file mode 100644 index 0000000..8c6cbe1 --- /dev/null +++ b/tests/endpoints/test_users.py @@ -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"] == [] diff --git a/tests/models/test_user.py b/tests/models/test_user.py new file mode 100644 index 0000000..f66cad3 --- /dev/null +++ b/tests/models/test_user.py @@ -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)