Skip to content

Commit c78ed5e

Browse files
committed
Add user_invites change approve function users.py --> user_invites.py
1 parent 9174f43 commit c78ed5e

File tree

4 files changed

+199
-40
lines changed

4 files changed

+199
-40
lines changed

permit/api/api_client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .role_assignments import RoleAssignmentsApi
1616
from .roles import RolesApi
1717
from .tenants import TenantsApi
18+
from .user_invites import UserInvitesApi
1819
from .users import UsersApi
1920

2021

@@ -43,6 +44,7 @@ def __init__(self, config: PermitConfig):
4344
self._relationship_tuples = RelationshipTuplesApi(config)
4445
self._roles = RolesApi(config)
4546
self._tenants = TenantsApi(config)
47+
self._user_invites = UserInvitesApi(config)
4648
self._users = UsersApi(config)
4749

4850
@property
@@ -165,6 +167,14 @@ def tenants(self) -> TenantsApi:
165167
"""
166168
return self._tenants
167169

170+
@property
171+
def user_invites(self) -> UserInvitesApi:
172+
"""
173+
API for managing user invites.
174+
See: https://api.permit.io/v2/redoc#tag/User-Invites
175+
"""
176+
return self._user_invites
177+
168178
@property
169179
def users(self) -> UsersApi:
170180
"""

permit/api/user_invites.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from typing import List, Optional, Union
2+
3+
from ..utils.pydantic_version import PYDANTIC_VERSION
4+
5+
if PYDANTIC_VERSION < (2, 0):
6+
from pydantic import validate_arguments
7+
else:
8+
from pydantic.v1 import validate_arguments
9+
10+
from .base import (
11+
BasePermitApi,
12+
SimpleHttpClient,
13+
pagination_params,
14+
)
15+
from .context import ApiContextLevel, ApiKeyAccessLevel
16+
from .models import (
17+
ElementsUserInviteApprove,
18+
ElementsUserInviteRead,
19+
)
20+
21+
22+
class UserInvitesApi(BasePermitApi):
23+
@property
24+
def __user_invites(self) -> SimpleHttpClient:
25+
return self._build_http_client(
26+
f"/v2/facts/{self.config.api_context.project}/{self.config.api_context.environment}/user_invites"
27+
)
28+
29+
@validate_arguments # type: ignore[operator]
30+
async def approve(self, user_invite_id: str, approve_data: ElementsUserInviteApprove) -> ElementsUserInviteRead:
31+
"""
32+
Approves a user invite.
33+
34+
Args:
35+
user_invite_id: The ID of the user invite to approve.
36+
approve_data: The approval data for the user invite.
37+
38+
Returns:
39+
the approved user invite.
40+
41+
Raises:
42+
PermitApiError: If the API returns an error HTTP status code.
43+
PermitContextError: If the configured ApiContext does not match the required endpoint context.
44+
"""
45+
await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
46+
await self._ensure_context(ApiContextLevel.ENVIRONMENT)
47+
return await self.__user_invites.post(
48+
f"/{user_invite_id}/approve",
49+
model=ElementsUserInviteRead,
50+
json=approve_data,
51+
)

permit/api/users.py

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
)
1515
from .context import ApiContextLevel, ApiKeyAccessLevel
1616
from .models import (
17-
ElementsUserInviteApprove,
18-
ElementsUserInviteRead,
1917
PaginatedResultUserRead,
2018
RoleAssignmentCreate,
2119
RoleAssignmentRead,
@@ -60,12 +58,6 @@ def __bulk_operations(self) -> SimpleHttpClient:
6058
f"/v2/facts/{self.config.api_context.project}/{self.config.api_context.environment}/bulk/users"
6159
)
6260

63-
@property
64-
def __user_invites(self) -> SimpleHttpClient:
65-
return self._build_http_client(
66-
f"/v2/facts/{self.config.api_context.project}/{self.config.api_context.environment}/user_invites"
67-
)
68-
6961
@validate_arguments # type: ignore[operator]
7062
async def list(self, page: int = 1, per_page: int = 100) -> PaginatedResultUserRead:
7163
"""
@@ -382,27 +374,3 @@ async def get_assigned_roles(
382374
model=List[RoleAssignmentRead],
383375
params=params,
384376
)
385-
386-
@validate_arguments # type: ignore[operator]
387-
async def approve(self, user_invite_id: str, approve_data: ElementsUserInviteApprove) -> ElementsUserInviteRead:
388-
"""
389-
Approves a user invite.
390-
391-
Args:
392-
user_invite_id: The ID of the user invite to approve.
393-
approve_data: The approval data for the user invite.
394-
395-
Returns:
396-
the approved user invite.
397-
398-
Raises:
399-
PermitApiError: If the API returns an error HTTP status code.
400-
PermitContextError: If the configured ApiContext does not match the required endpoint context.
401-
"""
402-
await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
403-
await self._ensure_context(ApiContextLevel.ENVIRONMENT)
404-
return await self.__user_invites.post(
405-
f"/{user_invite_id}/approve",
406-
model=ElementsUserInviteRead,
407-
json=approve_data,
408-
)

tests/test_user_invite_approve_simple.py

Lines changed: 138 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,28 @@
44
"""
55

66
import uuid
7+
from unittest.mock import AsyncMock, patch
78

89
import pytest
910

11+
from permit import Permit, PermitConfig
1012
from permit.api.models import ElementsUserInviteApprove, ElementsUserInviteRead, UserInviteStatus
11-
from permit.api.users import UsersApi
13+
from permit.api.user_invites import UserInvitesApi
1214

1315

1416
class TestUserInviteApproveSimple:
1517
"""Simple test suite for user invite approve functionality."""
1618

1719
def test_approve_method_exists(self):
18-
"""Test that the approve method exists in UsersApi."""
19-
assert hasattr(UsersApi, "approve")
20-
assert callable(UsersApi.approve)
20+
"""Test that the approve method exists in UserInvitesApi."""
21+
assert hasattr(UserInvitesApi, "approve")
22+
assert callable(UserInvitesApi.approve)
2123

2224
def test_approve_method_signature(self):
2325
"""Test that the approve method has the correct signature."""
2426
import inspect
2527

26-
method = UsersApi.approve
28+
method = UserInvitesApi.approve
2729
signature = inspect.signature(method)
2830

2931
# Check parameter names
@@ -57,8 +59,8 @@ def test_approve_data_model_key_validation(self):
5759
with pytest.raises(ValueError):
5860
ElementsUserInviteApprove(
5961
email="test@example.com",
60-
key="invalid key with spaces", # Should fail regex validation
61-
attributes={},
62+
key="invalid key with spaces",
63+
attributes={}, # Should fail regex validation
6264
)
6365

6466
def test_approve_data_model_with_complex_attributes(self):
@@ -121,7 +123,7 @@ def test_approve_parameter_types(self):
121123
"""Test that the approve method parameters have correct types."""
122124
from typing import get_type_hints
123125

124-
method = UsersApi.approve
126+
method = UserInvitesApi.approve
125127
type_hints = get_type_hints(method)
126128

127129
# Check parameter types
@@ -158,3 +160,131 @@ def test_approve_data_serialization(self):
158160
parsed_data = json.loads(json_str)
159161
assert parsed_data["email"] == "test@example.com"
160162
assert parsed_data["key"] == "test-key-123"
163+
164+
def test_permit_instance_has_user_invites_api(self):
165+
"""Test that Permit instance exposes user_invites API correctly."""
166+
# Create a Permit instance with mock configuration
167+
config = PermitConfig(token="test-token-123", api_url="http://localhost:8000", pdp="http://localhost:7766")
168+
permit = Permit(config)
169+
170+
# Test that user_invites API is accessible
171+
assert hasattr(permit.api, "user_invites")
172+
assert permit.api.user_invites is not None
173+
174+
# Test that the approve method is available
175+
assert hasattr(permit.api.user_invites, "approve")
176+
assert callable(permit.api.user_invites.approve)
177+
178+
@pytest.mark.asyncio
179+
async def test_permit_user_invites_approve_usage_pattern(self):
180+
"""
181+
Test the actual usage pattern: permit.api.user_invites.approve()
182+
This test mimics how users will actually use the function.
183+
"""
184+
# Create a Permit instance with mock configuration
185+
config = PermitConfig(token="test-token-123", api_url="http://localhost:8000", pdp="http://localhost:7766")
186+
permit = Permit(config)
187+
188+
# Create test data - this is how users will use it
189+
user_invite_id = "test-invite-uuid-123"
190+
approve_data = ElementsUserInviteApprove(
191+
email="newuser@company.com",
192+
key="new-user-invite-key",
193+
attributes={
194+
"department": "Engineering",
195+
"role": "Developer",
196+
"start_date": "2024-01-15",
197+
"manager": "manager@company.com",
198+
},
199+
)
200+
201+
# Mock the entire approve method to avoid HTTP calls
202+
mock_response = ElementsUserInviteRead(
203+
id=uuid.uuid4(),
204+
organization_id=uuid.uuid4(),
205+
project_id=uuid.uuid4(),
206+
environment_id=uuid.uuid4(),
207+
key="new-user-invite-key",
208+
status=UserInviteStatus.approved,
209+
email="newuser@company.com",
210+
first_name="New",
211+
last_name="User",
212+
role_id=uuid.uuid4(),
213+
tenant_id=uuid.uuid4(),
214+
created_at="2024-01-01T00:00:00Z",
215+
updated_at="2024-01-01T01:00:00Z",
216+
)
217+
218+
# Mock just the approve method
219+
with patch.object(permit.api.user_invites, "approve", new_callable=AsyncMock) as mock_approve:
220+
mock_approve.return_value = mock_response
221+
222+
# This is the actual usage pattern that users will use!
223+
result = await permit.api.user_invites.approve(user_invite_id, approve_data)
224+
225+
# Verify the result
226+
assert isinstance(result, ElementsUserInviteRead)
227+
assert result.status == UserInviteStatus.approved
228+
assert result.email == "newuser@company.com"
229+
assert result.key == "new-user-invite-key"
230+
231+
# Verify the method was called with correct parameters
232+
mock_approve.assert_called_once_with(user_invite_id, approve_data)
233+
234+
def test_user_invites_api_integration_in_permit_client(self):
235+
"""Test that the UserInvitesApi is properly integrated into the Permit client."""
236+
config = PermitConfig(token="test-token-123", api_url="http://localhost:8000", pdp="http://localhost:7766")
237+
permit = Permit(config)
238+
239+
# Test the complete integration
240+
assert hasattr(permit, "api")
241+
assert hasattr(permit.api, "user_invites")
242+
assert isinstance(permit.api.user_invites, UserInvitesApi)
243+
244+
# Test that it's different from users API
245+
assert hasattr(permit.api, "users")
246+
assert permit.api.user_invites is not permit.api.users
247+
assert type(permit.api.user_invites).__name__ == "UserInvitesApi"
248+
assert type(permit.api.users).__name__ == "UsersApi"
249+
250+
def test_actual_usage_pattern_structure(self):
251+
"""
252+
Test that the actual usage pattern permit.api.user_invites.approve() is available.
253+
This verifies the complete structure that users will interact with.
254+
"""
255+
# Create a real Permit instance (no mocking)
256+
config = PermitConfig(token="test-token-123", api_url="http://localhost:8000", pdp="http://localhost:7766")
257+
permit = Permit(config)
258+
259+
# Create real test data as users would
260+
approve_data = ElementsUserInviteApprove(
261+
email="newuser@company.com",
262+
key="new-user-invite-key",
263+
attributes={"department": "Engineering", "role": "Developer", "start_date": "2024-01-15"},
264+
)
265+
266+
# Verify the complete call chain exists (this is what users will use)
267+
assert hasattr(permit, "api"), "permit.api should exist"
268+
assert hasattr(permit.api, "user_invites"), "permit.api.user_invites should exist"
269+
assert hasattr(permit.api.user_invites, "approve"), "permit.api.user_invites.approve should exist"
270+
assert callable(permit.api.user_invites.approve), "permit.api.user_invites.approve should be callable"
271+
272+
# Verify the method signature matches what users expect
273+
import inspect
274+
275+
method = permit.api.user_invites.approve
276+
signature = inspect.signature(method)
277+
params = list(signature.parameters.keys())
278+
279+
assert "user_invite_id" in params, "Method should accept user_invite_id parameter"
280+
assert "approve_data" in params, "Method should accept approve_data parameter"
281+
282+
# Verify the approve_data can be created with user's data
283+
assert approve_data.email == "newuser@company.com"
284+
assert approve_data.key == "new-user-invite-key"
285+
assert approve_data.attributes["department"] == "Engineering"
286+
287+
288+
if __name__ == "__main__":
289+
# Run with: python -m pytest tests/test_user_invite_approve_simple.py -v
290+
pass

0 commit comments

Comments
 (0)