diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..d56caaa4 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(gh api:*)" + ] + } +} diff --git a/src/workos/authorization.py b/src/workos/authorization.py index c32e4454..c470c147 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -1,15 +1,22 @@ from enum import Enum +from functools import partial from typing import Any, Dict, Optional, Protocol, Sequence, Union from pydantic import TypeAdapter -from typing_extensions import TypedDict from workos.types.authorization.access_check_response import AccessCheckResponse +from workos.types.authorization.assignment import Assignment from workos.types.authorization.environment_role import ( EnvironmentRole, EnvironmentRoleList, ) +from workos.types.authorization.organization_membership import ( + AuthorizationOrganizationMembership, +) from workos.types.authorization.organization_role import OrganizationRole +from workos.types.authorization.parent_resource_identifier import ( + ParentResourceIdentifier, +) from workos.types.authorization.permission import Permission from workos.types.authorization.resource_identifier import ResourceIdentifier from workos.types.authorization.authorization_resource import AuthorizationResource @@ -60,16 +67,25 @@ class ResourceListFilters(ListArgs, total=False): ] -class ParentResourceById(TypedDict): - parent_resource_id: str +class ResourcesForMembershipListFilters(ListArgs, total=False): + permission_slug: str + + +AuthorizationResourcesForMembershipList = WorkOSListResource[ + AuthorizationResource, ResourcesForMembershipListFilters, ListMetadata +] -class ParentResourceByExternalId(TypedDict): - parent_resource_external_id: str - parent_resource_type_slug: str +class AuthorizationOrganizationMembershipListFilters(ListArgs, total=False): + permission_slug: str + assignment: Optional[Assignment] -ParentResource = Union[ParentResourceById, ParentResourceByExternalId] +AuthorizationOrganizationMembershipList = WorkOSListResource[ + AuthorizationOrganizationMembership, + AuthorizationOrganizationMembershipListFilters, + ListMetadata, +] _role_adapter: TypeAdapter[Role] = TypeAdapter(Role) @@ -224,7 +240,7 @@ def create_resource( description: Optional[str] = None, resource_type_slug: str, organization_id: str, - parent: Optional[ParentResource] = None, + parent: Optional[ParentResourceIdentifier] = None, ) -> SyncOrAsync[AuthorizationResource]: ... def update_resource( @@ -323,6 +339,44 @@ def list_role_assignments( order: PaginationOrder = "desc", ) -> SyncOrAsync[RoleAssignmentsListResource]: ... + def list_resources_for_membership( + self, + organization_membership_id: str, + *, + permission_slug: str, + parent_resource: ParentResourceIdentifier, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> SyncOrAsync[AuthorizationResourcesForMembershipList]: ... + + def list_memberships_for_resource( + self, + resource_id: str, + *, + permission_slug: str, + assignment: Optional[Assignment] = None, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> SyncOrAsync[AuthorizationOrganizationMembershipList]: ... + + def list_memberships_for_resource_by_external_id( + self, + organization_id: str, + resource_type_slug: str, + external_id: str, + *, + permission_slug: str, + assignment: Optional[Assignment] = None, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> SyncOrAsync[AuthorizationOrganizationMembershipList]: ... + class Authorization(AuthorizationModule): _http_client: SyncHTTPClient @@ -615,7 +669,7 @@ def create_resource( description: Optional[str] = None, resource_type_slug: str, organization_id: str, - parent: Optional[ParentResource] = None, + parent: Optional[ParentResourceIdentifier] = None, ) -> AuthorizationResource: json: Dict[str, Any] = { "resource_type_slug": resource_type_slug, @@ -790,7 +844,7 @@ def check( json.update(resource) response = self._http_client.request( - f"authorization/organization_memberships/{organization_membership_id}/check", + f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/check", method=REQUEST_METHOD_POST, json=json, ) @@ -880,6 +934,125 @@ def list_role_assignments( **ListPage[RoleAssignment](**response).model_dump(), ) + def list_resources_for_membership( + self, + organization_membership_id: str, + *, + permission_slug: str, + parent_resource: ParentResourceIdentifier, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> AuthorizationResourcesForMembershipList: + list_params: ResourcesForMembershipListFilters = { + "limit": limit, + "before": before, + "after": after, + "order": order, + "permission_slug": permission_slug, + } + + http_params: Dict[str, Any] = {**list_params} + http_params.update(parent_resource) + + response = self._http_client.request( + f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/resources", + method=REQUEST_METHOD_GET, + params=http_params, + ) + + return AuthorizationResourcesForMembershipList( + list_method=partial( + self.list_resources_for_membership, + organization_membership_id, + parent_resource=parent_resource, + ), + list_args=list_params, + **ListPage[AuthorizationResource](**response).model_dump(), + ) + + def list_memberships_for_resource( + self, + resource_id: str, + *, + permission_slug: str, + assignment: Optional[Assignment] = None, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> AuthorizationOrganizationMembershipList: + list_params: AuthorizationOrganizationMembershipListFilters = { + "limit": limit, + "before": before, + "after": after, + "order": order, + "permission_slug": permission_slug, + } + if assignment is not None: + list_params["assignment"] = assignment + + response = self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}/organization_memberships", + method=REQUEST_METHOD_GET, + params=list_params, + ) + + return WorkOSListResource[ + AuthorizationOrganizationMembership, + AuthorizationOrganizationMembershipListFilters, + ListMetadata, + ]( + list_method=partial(self.list_memberships_for_resource, resource_id), + list_args=list_params, + **ListPage[AuthorizationOrganizationMembership](**response).model_dump(), + ) + + def list_memberships_for_resource_by_external_id( + self, + organization_id: str, + resource_type_slug: str, + external_id: str, + *, + permission_slug: str, + assignment: Optional[Assignment] = None, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> AuthorizationOrganizationMembershipList: + list_params: AuthorizationOrganizationMembershipListFilters = { + "limit": limit, + "before": before, + "after": after, + "order": order, + "permission_slug": permission_slug, + } + if assignment is not None: + list_params["assignment"] = assignment + + response = self._http_client.request( + f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type_slug}/{external_id}/organization_memberships", + method=REQUEST_METHOD_GET, + params=list_params, + ) + + return WorkOSListResource[ + AuthorizationOrganizationMembership, + AuthorizationOrganizationMembershipListFilters, + ListMetadata, + ]( + list_method=partial( + self.list_memberships_for_resource_by_external_id, + organization_id, + resource_type_slug, + external_id, + ), + list_args=list_params, + **ListPage[AuthorizationOrganizationMembership](**response).model_dump(), + ) + class AsyncAuthorization(AuthorizationModule): _http_client: AsyncHTTPClient @@ -1174,7 +1347,7 @@ async def create_resource( description: Optional[str] = None, resource_type_slug: str, organization_id: str, - parent: Optional[ParentResource] = None, + parent: Optional[ParentResourceIdentifier] = None, ) -> AuthorizationResource: json: Dict[str, Any] = { "resource_type_slug": resource_type_slug, @@ -1349,7 +1522,7 @@ async def check( json.update(resource) response = await self._http_client.request( - f"authorization/organization_memberships/{organization_membership_id}/check", + f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/check", method=REQUEST_METHOD_POST, json=json, ) @@ -1438,3 +1611,122 @@ async def list_role_assignments( list_args=list_params, **ListPage[RoleAssignment](**response).model_dump(), ) + + async def list_resources_for_membership( + self, + organization_membership_id: str, + *, + permission_slug: str, + parent_resource: ParentResourceIdentifier, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> AuthorizationResourcesForMembershipList: + list_params: ResourcesForMembershipListFilters = { + "limit": limit, + "before": before, + "after": after, + "order": order, + "permission_slug": permission_slug, + } + + http_params: Dict[str, Any] = {**list_params} + http_params.update(parent_resource) + + response = await self._http_client.request( + f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/resources", + method=REQUEST_METHOD_GET, + params=http_params, + ) + + return AuthorizationResourcesForMembershipList( + list_method=partial( + self.list_resources_for_membership, + organization_membership_id, + parent_resource=parent_resource, + ), + list_args=list_params, + **ListPage[AuthorizationResource](**response).model_dump(), + ) + + async def list_memberships_for_resource( + self, + resource_id: str, + *, + permission_slug: str, + assignment: Optional[Assignment] = None, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> AuthorizationOrganizationMembershipList: + list_params: AuthorizationOrganizationMembershipListFilters = { + "limit": limit, + "before": before, + "after": after, + "order": order, + "permission_slug": permission_slug, + } + if assignment is not None: + list_params["assignment"] = assignment + + response = await self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}/organization_memberships", + method=REQUEST_METHOD_GET, + params=list_params, + ) + + return WorkOSListResource[ + AuthorizationOrganizationMembership, + AuthorizationOrganizationMembershipListFilters, + ListMetadata, + ]( + list_method=partial(self.list_memberships_for_resource, resource_id), + list_args=list_params, + **ListPage[AuthorizationOrganizationMembership](**response).model_dump(), + ) + + async def list_memberships_for_resource_by_external_id( + self, + organization_id: str, + resource_type_slug: str, + external_id: str, + *, + permission_slug: str, + assignment: Optional[Assignment] = None, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> AuthorizationOrganizationMembershipList: + list_params: AuthorizationOrganizationMembershipListFilters = { + "limit": limit, + "before": before, + "after": after, + "order": order, + "permission_slug": permission_slug, + } + if assignment is not None: + list_params["assignment"] = assignment + + response = await self._http_client.request( + f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type_slug}/{external_id}/organization_memberships", + method=REQUEST_METHOD_GET, + params=list_params, + ) + + return WorkOSListResource[ + AuthorizationOrganizationMembership, + AuthorizationOrganizationMembershipListFilters, + ListMetadata, + ]( + list_method=partial( + self.list_memberships_for_resource_by_external_id, + organization_id, + resource_type_slug, + external_id, + ), + list_args=list_params, + **ListPage[AuthorizationOrganizationMembership](**response).model_dump(), + ) diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py index 9b5dcdab..a6c93040 100644 --- a/src/workos/types/authorization/__init__.py +++ b/src/workos/types/authorization/__init__.py @@ -12,6 +12,9 @@ OrganizationRoleList, ) from workos.types.authorization.permission import Permission +from workos.types.authorization.parent_resource_identifier import ( + ParentResourceIdentifier, +) from workos.types.authorization.authorization_resource import AuthorizationResource from workos.types.authorization.resource_identifier import ( ResourceIdentifier, diff --git a/src/workos/types/authorization/assignment.py b/src/workos/types/authorization/assignment.py new file mode 100644 index 00000000..ea87fca9 --- /dev/null +++ b/src/workos/types/authorization/assignment.py @@ -0,0 +1,3 @@ +from typing import Literal + +Assignment = Literal["direct", "indirect"] diff --git a/src/workos/types/authorization/parent_resource_identifier.py b/src/workos/types/authorization/parent_resource_identifier.py new file mode 100644 index 00000000..c105f87b --- /dev/null +++ b/src/workos/types/authorization/parent_resource_identifier.py @@ -0,0 +1,15 @@ +from typing import Union + +from typing_extensions import TypedDict + + +class ParentResourceById(TypedDict): + parent_resource_id: str + + +class ParentResourceByExternalId(TypedDict): + parent_resource_external_id: str + parent_resource_type_slug: str + + +ParentResourceIdentifier = Union[ParentResourceById, ParentResourceByExternalId] diff --git a/tests/test_authorization_resource_memberships.py b/tests/test_authorization_resource_memberships.py new file mode 100644 index 00000000..bcaf2107 --- /dev/null +++ b/tests/test_authorization_resource_memberships.py @@ -0,0 +1,904 @@ +from typing import Union + +import pytest +from tests.utils.fixtures.mock_organization_membership import ( + MockAuthorizationOrganizationMembershipList, +) +from tests.utils.fixtures.mock_resource_list import MockAuthorizationResourceList +from tests.utils.syncify import syncify +from workos.authorization import AsyncAuthorization, Authorization +from workos.types.authorization.parent_resource_identifier import ( + ParentResourceByExternalId, + ParentResourceById, +) + + +@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) +class TestListResourcesForMembership: + @pytest.fixture(autouse=True) + def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): + self.http_client = module_instance._http_client + self.authorization = module_instance + + @pytest.fixture + def mock_resources_list(self): + return MockAuthorizationResourceList().model_dump() + + def test_list_resources_for_membership_with_parent_by_id_returns_paginated_list( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceById(parent_resource_id="res_parent_123") + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + response = syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01ABC/resources" + ) + assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + assert response.object == "list" + assert len(response.data) == 2 + assert response.data[0].object == "authorization_resource" + assert response.data[0].id == "authz_resource_01HXYZ123ABC456DEF789ABC" + assert response.data[0].external_id == "doc-12345678" + assert response.data[0].name == "Q5 Budget Report" + assert response.data[0].description == "Financial report for Q5 2025" + assert response.data[0].resource_type_slug == "document" + assert response.data[0].organization_id == "org_01HXYZ123ABC456DEF789ABC" + assert ( + response.data[0].parent_resource_id + == "authz_resource_01HXYZ123ABC456DEF789XYZ" + ) + assert response.data[0].created_at == "2024-01-15T09:30:00.000Z" + assert response.data[0].updated_at == "2024-01-15T09:30:00.000Z" + assert response.data[1].object == "authorization_resource" + assert response.data[1].id == "authz_resource_01HXYZ123ABC456DEF789DEF" + assert response.data[1].external_id == "folder-123" + assert response.data[1].name == "Finance Folder" + assert response.data[1].description is None + assert response.data[1].resource_type_slug == "folder" + assert response.data[1].organization_id == "org_01HXYZ123ABC456DEF789ABC" + assert response.data[1].parent_resource_id is None + assert response.data[1].created_at == "2024-01-14T08:00:00.000Z" + assert response.data[1].updated_at == "2024-01-14T08:00:00.000Z" + assert response.list_metadata.before is None + assert response.list_metadata.after == "authz_resource_01HXYZ123ABC456DEF789DEF" + + def test_list_resources_for_membership_with_parent_by_id_with_limit( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceById(parent_resource_id="res_parent_123") + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + limit=25, + ) + ) + + assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["limit"] == 25 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_resources_for_membership_with_parent_by_id_with_before( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceById(parent_resource_id="res_parent_123") + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + before="cursor_before", + ) + ) + + assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" + assert request_kwargs["params"]["before"] == "cursor_before" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_resources_for_membership_with_parent_by_id_with_after( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceById(parent_resource_id="res_parent_123") + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + after="cursor_after", + ) + ) + + assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" + assert request_kwargs["params"]["after"] == "cursor_after" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_resources_for_membership_with_parent_by_id_with_order_asc( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceById(parent_resource_id="res_parent_123") + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + order="asc", + ) + ) + + assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" + assert request_kwargs["params"]["order"] == "asc" + assert request_kwargs["params"]["limit"] == 10 + + def test_list_resources_for_membership_with_parent_by_id_with_order_desc( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceById(parent_resource_id="res_parent_123") + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + order="desc", + ) + ) + + assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" + assert request_kwargs["params"]["order"] == "desc" + assert request_kwargs["params"]["limit"] == 10 + + def test_list_resources_for_membership_with_parent_by_id_with_all_parameters( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceById(parent_resource_id="res_parent_123") + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + response = syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + limit=5, + before="cursor_before", + after="cursor_after", + order="asc", + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01ABC/resources" + ) + assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["limit"] == 5 + assert request_kwargs["params"]["before"] == "cursor_before" + assert request_kwargs["params"]["after"] == "cursor_after" + assert request_kwargs["params"]["order"] == "asc" + + assert response.object == "list" + assert len(response.data) == 2 + + # --- list_resources_for_membership with ParentResourceByExternalId --- + + def test_list_resources_for_membership_with_parent_by_external_id_returns_paginated_list( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceByExternalId( + parent_resource_external_id="parent_ext_456", + parent_resource_type_slug="folder", + ) + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + response = syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01ABC/resources" + ) + assert ( + request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" + ) + assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + assert "parent_resource_id" not in request_kwargs["params"] + + assert response.object == "list" + assert len(response.data) == 2 + assert response.data[0].object == "authorization_resource" + assert response.data[0].id == "authz_resource_01HXYZ123ABC456DEF789ABC" + assert response.data[0].external_id == "doc-12345678" + assert response.data[0].name == "Q5 Budget Report" + assert response.data[0].description == "Financial report for Q5 2025" + assert response.data[0].resource_type_slug == "document" + assert response.data[0].organization_id == "org_01HXYZ123ABC456DEF789ABC" + assert ( + response.data[0].parent_resource_id + == "authz_resource_01HXYZ123ABC456DEF789XYZ" + ) + assert response.data[0].created_at == "2024-01-15T09:30:00.000Z" + assert response.data[0].updated_at == "2024-01-15T09:30:00.000Z" + assert response.data[1].object == "authorization_resource" + assert response.data[1].id == "authz_resource_01HXYZ123ABC456DEF789DEF" + assert response.data[1].external_id == "folder-123" + assert response.data[1].name == "Finance Folder" + assert response.data[1].description is None + assert response.data[1].resource_type_slug == "folder" + assert response.data[1].organization_id == "org_01HXYZ123ABC456DEF789ABC" + assert response.data[1].parent_resource_id is None + assert response.data[1].created_at == "2024-01-14T08:00:00.000Z" + assert response.data[1].updated_at == "2024-01-14T08:00:00.000Z" + assert response.list_metadata.before is None + + def test_list_resources_for_membership_with_parent_by_external_id_with_limit( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceByExternalId( + parent_resource_external_id="parent_ext_456", + parent_resource_type_slug="folder", + ) + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + limit=25, + ) + ) + + assert ( + request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" + ) + assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" + assert request_kwargs["params"]["limit"] == 25 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_resources_for_membership_with_parent_by_external_id_with_before( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceByExternalId( + parent_resource_external_id="parent_ext_456", + parent_resource_type_slug="folder", + ) + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + before="cursor_before", + ) + ) + + assert ( + request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" + ) + assert request_kwargs["params"]["before"] == "cursor_before" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_resources_for_membership_with_parent_by_external_id_with_after( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceByExternalId( + parent_resource_external_id="parent_ext_456", + parent_resource_type_slug="folder", + ) + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + after="cursor_after", + ) + ) + + assert ( + request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" + ) + assert request_kwargs["params"]["after"] == "cursor_after" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_resources_for_membership_with_parent_by_external_id_with_order_asc( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceByExternalId( + parent_resource_external_id="parent_ext_456", + parent_resource_type_slug="folder", + ) + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + order="asc", + ) + ) + + assert ( + request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" + ) + assert request_kwargs["params"]["order"] == "asc" + assert request_kwargs["params"]["limit"] == 10 + + def test_list_resources_for_membership_with_parent_by_external_id_with_order_desc( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceByExternalId( + parent_resource_external_id="parent_ext_456", + parent_resource_type_slug="folder", + ) + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + order="desc", + ) + ) + + assert ( + request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" + ) + assert request_kwargs["params"]["order"] == "desc" + assert request_kwargs["params"]["limit"] == 10 + + def test_list_resources_for_membership_with_parent_by_external_id_with_all_parameters( + self, mock_resources_list, capture_and_mock_http_client_request + ): + parent = ParentResourceByExternalId( + parent_resource_external_id="parent_ext_456", + parent_resource_type_slug="folder", + ) + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + response = syncify( + self.authorization.list_resources_for_membership( + "om_01ABC", + permission_slug="document:read", + parent_resource=parent, + limit=5, + before="cursor_before", + after="cursor_after", + order="asc", + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01ABC/resources" + ) + assert ( + request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" + ) + assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["limit"] == 5 + assert request_kwargs["params"]["before"] == "cursor_before" + assert request_kwargs["params"]["after"] == "cursor_after" + assert request_kwargs["params"]["order"] == "asc" + assert "parent_resource_id" not in request_kwargs["params"] + + assert response.object == "list" + assert len(response.data) == 2 + + +@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) +class TestListMembershipsForResource: + @pytest.fixture(autouse=True) + def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): + self.http_client = module_instance._http_client + self.authorization = module_instance + + @pytest.fixture + def mock_memberships_list_two(self): + return MockAuthorizationOrganizationMembershipList().model_dump() + + # --- list_memberships_for_resource (by resource_id) --- + + def test_list_memberships_for_resource_returns_paginated_list( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + response = syncify( + self.authorization.list_memberships_for_resource( + "authz_resource_01HXYZ123ABC456DEF789ABC", + permission_slug="document:read", + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith( + "/authorization/resources/authz_resource_01HXYZ123ABC456DEF789ABC/organization_memberships" + ) + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + assert response.object == "list" + assert len(response.data) == 2 + assert response.data[0].object == "organization_membership" + assert response.data[0].id == "om_01ABC" + assert response.data[0].user_id == "user_123" + assert response.data[0].organization_id == "org_456" + assert response.data[0].status == "active" + assert response.data[0].created_at == "2024-01-01T00:00:00Z" + assert response.data[0].updated_at == "2024-01-01T00:00:00Z" + assert response.data[1].object == "organization_membership" + assert response.data[1].id == "om_01DEF" + assert response.data[1].user_id == "user_789" + assert response.data[1].organization_id == "org_456" + assert response.data[1].status == "active" + assert response.data[1].created_at == "2024-01-02T00:00:00Z" + assert response.data[1].updated_at == "2024-01-02T00:00:00Z" + assert response.list_metadata.before is None + assert response.list_metadata.after == "om_01DEF" + + def test_list_memberships_for_resource_with_assignment_direct( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource( + "authz_resource_01HXYZ", + permission_slug="document:read", + assignment="direct", + ) + ) + + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["assignment"] == "direct" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_memberships_for_resource_with_assignment_indirect( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource( + "authz_resource_01HXYZ", + permission_slug="document:read", + assignment="indirect", + ) + ) + + assert request_kwargs["params"]["assignment"] == "indirect" + assert request_kwargs["params"]["permission_slug"] == "document:read" + + def test_list_memberships_for_resource_with_limit( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource( + "authz_resource_01HXYZ", + permission_slug="document:read", + limit=25, + ) + ) + + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["limit"] == 25 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_memberships_for_resource_with_before( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource( + "authz_resource_01HXYZ", + permission_slug="document:read", + before="cursor_before", + ) + ) + + assert request_kwargs["params"]["before"] == "cursor_before" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_memberships_for_resource_with_after( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource( + "authz_resource_01HXYZ", + permission_slug="document:read", + after="cursor_after", + ) + ) + + assert request_kwargs["params"]["after"] == "cursor_after" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_memberships_for_resource_with_order_asc( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource( + "authz_resource_01HXYZ", + permission_slug="document:read", + order="asc", + ) + ) + + assert request_kwargs["params"]["order"] == "asc" + assert request_kwargs["params"]["limit"] == 10 + + def test_list_memberships_for_resource_with_order_desc( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource( + "authz_resource_01HXYZ", + permission_slug="document:read", + order="desc", + ) + ) + + assert request_kwargs["params"]["order"] == "desc" + assert request_kwargs["params"]["limit"] == 10 + + def test_list_memberships_for_resource_with_all_parameters( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + response = syncify( + self.authorization.list_memberships_for_resource( + "authz_resource_01HXYZ", + permission_slug="document:read", + assignment="direct", + limit=5, + before="cursor_before", + after="cursor_after", + order="asc", + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith( + "/authorization/resources/authz_resource_01HXYZ/organization_memberships" + ) + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["assignment"] == "direct" + assert request_kwargs["params"]["limit"] == 5 + assert request_kwargs["params"]["before"] == "cursor_before" + assert request_kwargs["params"]["after"] == "cursor_after" + assert request_kwargs["params"]["order"] == "asc" + + assert response.object == "list" + assert len(response.data) == 2 + + # --- list_memberships_for_resource_by_external_id --- + + def test_list_memberships_for_resource_by_external_id_returns_paginated_list( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + response = syncify( + self.authorization.list_memberships_for_resource_by_external_id( + organization_id="org_123", + resource_type_slug="document", + external_id="doc-ext-456", + permission_slug="document:read", + ) + ) + + assert request_kwargs["method"] == "get" + assert ( + "/authorization/organizations/org_123/resources/document/doc-ext-456/organization_memberships" + in request_kwargs["url"] + ) + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + assert response.object == "list" + assert len(response.data) == 2 + assert response.data[0].object == "organization_membership" + assert response.data[0].id == "om_01ABC" + assert response.data[0].user_id == "user_123" + assert response.data[0].organization_id == "org_456" + assert response.data[0].status == "active" + assert response.data[0].created_at == "2024-01-01T00:00:00Z" + assert response.data[0].updated_at == "2024-01-01T00:00:00Z" + assert response.data[1].object == "organization_membership" + assert response.data[1].id == "om_01DEF" + assert response.data[1].user_id == "user_789" + assert response.data[1].organization_id == "org_456" + assert response.data[1].status == "active" + assert response.data[1].created_at == "2024-01-02T00:00:00Z" + assert response.data[1].updated_at == "2024-01-02T00:00:00Z" + assert response.list_metadata.before is None + assert response.list_metadata.after == "om_01DEF" + + def test_list_memberships_for_resource_by_external_id_with_assignment_direct( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource_by_external_id( + organization_id="org_123", + resource_type_slug="document", + external_id="doc-ext-456", + permission_slug="document:read", + assignment="direct", + ) + ) + + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["assignment"] == "direct" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_memberships_for_resource_by_external_id_with_assignment_indirect( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource_by_external_id( + organization_id="org_123", + resource_type_slug="folder", + external_id="folder-ext-789", + permission_slug="document:read", + assignment="indirect", + ) + ) + + assert request_kwargs["params"]["assignment"] == "indirect" + assert ( + "/authorization/organizations/org_123/resources/folder/folder-ext-789/organization_memberships" + in request_kwargs["url"] + ) + + def test_list_memberships_for_resource_by_external_id_with_limit( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource_by_external_id( + organization_id="org_123", + resource_type_slug="document", + external_id="doc-ext-456", + permission_slug="document:read", + limit=25, + ) + ) + + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["limit"] == 25 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_memberships_for_resource_by_external_id_with_before( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource_by_external_id( + organization_id="org_123", + resource_type_slug="document", + external_id="doc-ext-456", + permission_slug="document:read", + before="cursor_before", + ) + ) + + assert request_kwargs["params"]["before"] == "cursor_before" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_memberships_for_resource_by_external_id_with_after( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource_by_external_id( + organization_id="org_123", + resource_type_slug="document", + external_id="doc-ext-456", + permission_slug="document:read", + after="cursor_after", + ) + ) + + assert request_kwargs["params"]["after"] == "cursor_after" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + def test_list_memberships_for_resource_by_external_id_with_order_asc( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource_by_external_id( + organization_id="org_123", + resource_type_slug="document", + external_id="doc-ext-456", + permission_slug="document:read", + order="asc", + ) + ) + + assert request_kwargs["params"]["order"] == "asc" + assert request_kwargs["params"]["limit"] == 10 + + def test_list_memberships_for_resource_by_external_id_with_order_desc( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + syncify( + self.authorization.list_memberships_for_resource_by_external_id( + organization_id="org_123", + resource_type_slug="document", + external_id="doc-ext-456", + permission_slug="document:read", + order="desc", + ) + ) + + assert request_kwargs["params"]["order"] == "desc" + assert request_kwargs["params"]["limit"] == 10 + + def test_list_memberships_for_resource_by_external_id_with_all_parameters( + self, mock_memberships_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_memberships_list_two, 200 + ) + + response = syncify( + self.authorization.list_memberships_for_resource_by_external_id( + organization_id="org_123", + resource_type_slug="document", + external_id="doc-ext-456", + permission_slug="document:read", + assignment="direct", + limit=5, + before="cursor_before", + after="cursor_after", + order="asc", + ) + ) + + assert request_kwargs["method"] == "get" + assert ( + "/authorization/organizations/org_123/resources/document/doc-ext-456/organization_memberships" + in request_kwargs["url"] + ) + assert request_kwargs["params"]["permission_slug"] == "document:read" + assert request_kwargs["params"]["assignment"] == "direct" + assert request_kwargs["params"]["limit"] == 5 + assert request_kwargs["params"]["before"] == "cursor_before" + assert request_kwargs["params"]["after"] == "cursor_after" + assert request_kwargs["params"]["order"] == "asc" + + assert response.object == "list" + assert len(response.data) == 2 diff --git a/tests/utils/fixtures/mock_organization_membership.py b/tests/utils/fixtures/mock_organization_membership.py index b363b48b..9314fd14 100644 --- a/tests/utils/fixtures/mock_organization_membership.py +++ b/tests/utils/fixtures/mock_organization_membership.py @@ -1,8 +1,50 @@ import datetime +from typing import Optional, Sequence +from workos.types.authorization.organization_membership import ( + AuthorizationOrganizationMembership, +) +from workos.types.list_resource import ListMetadata, ListPage from workos.types.user_management import OrganizationMembership +class MockAuthorizationOrganizationMembershipList( + ListPage[AuthorizationOrganizationMembership] +): + def __init__( + self, + data: Optional[Sequence[AuthorizationOrganizationMembership]] = None, + before: Optional[str] = None, + after: Optional[str] = "om_01DEF", + ): + if data is None: + data = [ + AuthorizationOrganizationMembership( + object="organization_membership", + id="om_01ABC", + user_id="user_123", + organization_id="org_456", + status="active", + created_at="2024-01-01T00:00:00Z", + updated_at="2024-01-01T00:00:00Z", + ), + AuthorizationOrganizationMembership( + object="organization_membership", + id="om_01DEF", + user_id="user_789", + organization_id="org_456", + status="active", + created_at="2024-01-02T00:00:00Z", + updated_at="2024-01-02T00:00:00Z", + ), + ] + super().__init__( + object="list", + data=data, + list_metadata=ListMetadata(before=before, after=after), + ) + + class MockOrganizationMembership(OrganizationMembership): def __init__(self, id): now = datetime.datetime.now().isoformat()