From ab35617fdaef0d0fc49bd5b65203636a6c9e4e81 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Thu, 19 Feb 2026 09:02:10 -1000 Subject: [PATCH 01/42] adding base types and module registraiton --- src/workos/types/authorization/__init__.py | 6 ++++ .../types/authorization/access_evaluation.py | 7 ++++ .../authorization/organization_membership.py | 23 ++++++++++++ src/workos/types/authorization/resource.py | 18 ++++++++++ .../types/authorization/role_assignment.py | 13 +++++++ src/workos/types/list_resource.py | 8 +++++ src/workos/utils/_base_http_client.py | 6 ++-- src/workos/utils/http_client.py | 36 ++++++++++++++++++- 8 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/workos/types/authorization/access_evaluation.py create mode 100644 src/workos/types/authorization/organization_membership.py create mode 100644 src/workos/types/authorization/resource.py create mode 100644 src/workos/types/authorization/role_assignment.py diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py index 609a4f2d..ecdafa79 100644 --- a/src/workos/types/authorization/__init__.py +++ b/src/workos/types/authorization/__init__.py @@ -1,14 +1,20 @@ +from workos.types.authorization.access_evaluation import AccessEvaluation 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, OrganizationRoleEvent, OrganizationRoleList, ) from workos.types.authorization.permission import Permission +from workos.types.authorization.resource import Resource from workos.types.authorization.role import ( Role, RoleList, ) +from workos.types.authorization.role_assignment import RoleAssignment diff --git a/src/workos/types/authorization/access_evaluation.py b/src/workos/types/authorization/access_evaluation.py new file mode 100644 index 00000000..6b2a22af --- /dev/null +++ b/src/workos/types/authorization/access_evaluation.py @@ -0,0 +1,7 @@ +from workos.types.workos_model import WorkOSModel + + +class AccessEvaluation(WorkOSModel): + """Representation of a WorkOS Authorization access check result.""" + + authorized: bool diff --git a/src/workos/types/authorization/organization_membership.py b/src/workos/types/authorization/organization_membership.py new file mode 100644 index 00000000..1ddc3ef7 --- /dev/null +++ b/src/workos/types/authorization/organization_membership.py @@ -0,0 +1,23 @@ +from typing import Any, Literal, Mapping, Optional + +from workos.types.workos_model import WorkOSModel +from workos.typing.literals import LiteralOrUntyped + +OrganizationMembershipStatus = Literal["active", "inactive", "pending"] + + +class AuthorizationOrganizationMembership(WorkOSModel): + """Representation of an Organization Membership returned by Authorization endpoints. + + This is a separate type from the user_management OrganizationMembership because + authorization endpoints return memberships without `role` and `organization_name` fields. + """ + + object: Literal["organization_membership"] + id: str + user_id: str + organization_id: str + status: LiteralOrUntyped[OrganizationMembershipStatus] + custom_attributes: Optional[Mapping[str, Any]] = None + created_at: str + updated_at: str diff --git a/src/workos/types/authorization/resource.py b/src/workos/types/authorization/resource.py new file mode 100644 index 00000000..65aef33b --- /dev/null +++ b/src/workos/types/authorization/resource.py @@ -0,0 +1,18 @@ +from typing import Any, Literal, Mapping, Optional + +from workos.types.workos_model import WorkOSModel + + +class Resource(WorkOSModel): + """Representation of a WorkOS Authorization Resource.""" + + object: Literal["authorization_resource"] + id: str + resource_type: str + resource_id: str + organization_id: str + external_id: Optional[str] = None + meta: Optional[Mapping[str, Any]] = None + environment_id: str + created_at: str + updated_at: str diff --git a/src/workos/types/authorization/role_assignment.py b/src/workos/types/authorization/role_assignment.py new file mode 100644 index 00000000..e06c04c2 --- /dev/null +++ b/src/workos/types/authorization/role_assignment.py @@ -0,0 +1,13 @@ +from typing import Literal + +from workos.types.workos_model import WorkOSModel + + +class RoleAssignment(WorkOSModel): + """Representation of a WorkOS Authorization Role Assignment.""" + + object: Literal["role_assignment"] + id: str + role_slug: str + role_name: str + role_id: str diff --git a/src/workos/types/list_resource.py b/src/workos/types/list_resource.py index e8621ab9..db07dcf0 100644 --- a/src/workos/types/list_resource.py +++ b/src/workos/types/list_resource.py @@ -19,7 +19,12 @@ from typing_extensions import Required, TypedDict from workos.types.api_keys import ApiKey from workos.types.audit_logs import AuditLogAction, AuditLogSchema +from workos.types.authorization.organization_membership import ( + AuthorizationOrganizationMembership, +) from workos.types.authorization.permission import Permission +from workos.types.authorization.resource import Resource +from workos.types.authorization.role_assignment import RoleAssignment from workos.types.directory_sync import ( Directory, DirectoryGroup, @@ -59,6 +64,9 @@ Organization, OrganizationMembership, Permission, + Resource, + RoleAssignment, + AuthorizationOrganizationMembership, AuthorizationResource, AuthorizationResourceType, User, diff --git a/src/workos/utils/_base_http_client.py b/src/workos/utils/_base_http_client.py index 49dcbcf5..e2164f1a 100644 --- a/src/workos/utils/_base_http_client.py +++ b/src/workos/utils/_base_http_client.py @@ -123,6 +123,7 @@ def _prepare_request( json: JsonType = None, headers: HeadersType = None, exclude_default_auth_headers: bool = False, + force_include_body: bool = False, ) -> PreparedRequest: """Executes a request against the WorkOS API. @@ -134,6 +135,7 @@ def _prepare_request( params Optional[dict]: Query params or body payload to be added to the request headers Optional[dict]: Custom headers to be added to the request token Optional[str]: Bearer token + force_include_body (bool): If True, allows sending a body with DELETE requests Returns: dict: Response from WorkOS @@ -149,7 +151,7 @@ def _prepare_request( REQUEST_METHOD_GET, ] - if bodyless_http_method and json is not None: + if bodyless_http_method and json is not None and not force_include_body: raise ValueError(f"Cannot send a body with a {parsed_method} request") # Remove any parameters that are None @@ -161,7 +163,7 @@ def _prepare_request( json = {k: v for k, v in json.items() if v is not None} # We'll spread these return values onto the HTTP client request method - if bodyless_http_method: + if bodyless_http_method and not force_include_body: return { "method": parsed_method, "url": url, diff --git a/src/workos/utils/http_client.py b/src/workos/utils/http_client.py index 203c7df0..0af5834c 100644 --- a/src/workos/utils/http_client.py +++ b/src/workos/utils/http_client.py @@ -14,7 +14,7 @@ ParamsType, ResponseJson, ) -from workos.utils.request_helper import REQUEST_METHOD_GET +from workos.utils.request_helper import REQUEST_METHOD_DELETE, REQUEST_METHOD_GET class SyncHttpxClientWrapper(httpx.Client): @@ -113,6 +113,23 @@ def request( response = self._client.request(**prepared_request_parameters) return self._handle_response(response) + def delete_with_body( + self, + path: str, + json: JsonType = None, + headers: HeadersType = None, + ) -> ResponseJson: + """Executes a DELETE request with a JSON body against the WorkOS API.""" + prepared_request_parameters = self._prepare_request( + path=path, + method=REQUEST_METHOD_DELETE, + json=json, + headers=headers, + force_include_body=True, + ) + response = self._client.request(**prepared_request_parameters) + return self._handle_response(response) + class AsyncHttpxClientWrapper(httpx.AsyncClient): def __del__(self) -> None: @@ -210,5 +227,22 @@ async def request( response = await self._client.request(**prepared_request_parameters) return self._handle_response(response) + async def delete_with_body( + self, + path: str, + json: JsonType = None, + headers: HeadersType = None, + ) -> ResponseJson: + """Executes a DELETE request with a JSON body against the WorkOS API.""" + prepared_request_parameters = self._prepare_request( + path=path, + method=REQUEST_METHOD_DELETE, + json=json, + headers=headers, + force_include_body=True, + ) + response = await self._client.request(**prepared_request_parameters) + return self._handle_response(response) + HTTPClient = Union[AsyncHTTPClient, SyncHTTPClient] From 5f6892d3c6f0dbc017ca23cc4fd7e769489a583b Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Thu, 19 Feb 2026 11:00:21 -1000 Subject: [PATCH 02/42] moar --- src/workos/types/authorization/__init__.py | 6 +++++- .../authorization/organization_membership.py | 3 ++- src/workos/types/authorization/resource.py | 10 +++++---- .../types/authorization/role_assignment.py | 21 ++++++++++++++++--- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py index ecdafa79..9eb705a0 100644 --- a/src/workos/types/authorization/__init__.py +++ b/src/workos/types/authorization/__init__.py @@ -17,4 +17,8 @@ Role, RoleList, ) -from workos.types.authorization.role_assignment import RoleAssignment +from workos.types.authorization.role_assignment import ( + RoleAssignment, + RoleAssignmentResource, + RoleAssignmentRole, +) diff --git a/src/workos/types/authorization/organization_membership.py b/src/workos/types/authorization/organization_membership.py index 1ddc3ef7..73d1983c 100644 --- a/src/workos/types/authorization/organization_membership.py +++ b/src/workos/types/authorization/organization_membership.py @@ -10,13 +10,14 @@ class AuthorizationOrganizationMembership(WorkOSModel): """Representation of an Organization Membership returned by Authorization endpoints. This is a separate type from the user_management OrganizationMembership because - authorization endpoints return memberships without `role` and `organization_name` fields. + authorization endpoints return memberships without the `role` field. """ object: Literal["organization_membership"] id: str user_id: str organization_id: str + organization_name: str status: LiteralOrUntyped[OrganizationMembershipStatus] custom_attributes: Optional[Mapping[str, Any]] = None created_at: str diff --git a/src/workos/types/authorization/resource.py b/src/workos/types/authorization/resource.py index 65aef33b..e3525ffe 100644 --- a/src/workos/types/authorization/resource.py +++ b/src/workos/types/authorization/resource.py @@ -8,11 +8,13 @@ class Resource(WorkOSModel): object: Literal["authorization_resource"] id: str - resource_type: str - resource_id: str + external_id: str + name: str + description: Optional[str] = None + resource_type_slug: str organization_id: str - external_id: Optional[str] = None + parent_resource_id: Optional[str] = None meta: Optional[Mapping[str, Any]] = None - environment_id: str + environment_id: Optional[str] = None created_at: str updated_at: str diff --git a/src/workos/types/authorization/role_assignment.py b/src/workos/types/authorization/role_assignment.py index e06c04c2..784eb589 100644 --- a/src/workos/types/authorization/role_assignment.py +++ b/src/workos/types/authorization/role_assignment.py @@ -3,11 +3,26 @@ from workos.types.workos_model import WorkOSModel +class RoleAssignmentRole(WorkOSModel): + """The role associated with a role assignment.""" + + slug: str + + +class RoleAssignmentResource(WorkOSModel): + """The resource associated with a role assignment.""" + + id: str + external_id: str + resource_type_slug: str + + class RoleAssignment(WorkOSModel): """Representation of a WorkOS Authorization Role Assignment.""" object: Literal["role_assignment"] id: str - role_slug: str - role_name: str - role_id: str + role: RoleAssignmentRole + resource: RoleAssignmentResource + created_at: str + updated_at: str From 0bc40fb39b5d4ff4a1171dc4ae020eab3e4bd249 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Thu, 19 Feb 2026 14:27:27 -1000 Subject: [PATCH 03/42] lol --- .../authorization/{resource.py => authorization_resource.py} | 5 +---- src/workos/types/authorization/role_assignment.py | 3 --- src/workos/utils/_base_http_client.py | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) rename src/workos/types/authorization/{resource.py => authorization_resource.py} (68%) diff --git a/src/workos/types/authorization/resource.py b/src/workos/types/authorization/authorization_resource.py similarity index 68% rename from src/workos/types/authorization/resource.py rename to src/workos/types/authorization/authorization_resource.py index e3525ffe..769d93fd 100644 --- a/src/workos/types/authorization/resource.py +++ b/src/workos/types/authorization/authorization_resource.py @@ -3,8 +3,7 @@ from workos.types.workos_model import WorkOSModel -class Resource(WorkOSModel): - """Representation of a WorkOS Authorization Resource.""" +class AuthorizationResource(WorkOSModel): object: Literal["authorization_resource"] id: str @@ -14,7 +13,5 @@ class Resource(WorkOSModel): resource_type_slug: str organization_id: str parent_resource_id: Optional[str] = None - meta: Optional[Mapping[str, Any]] = None - environment_id: Optional[str] = None created_at: str updated_at: str diff --git a/src/workos/types/authorization/role_assignment.py b/src/workos/types/authorization/role_assignment.py index 784eb589..5d0888de 100644 --- a/src/workos/types/authorization/role_assignment.py +++ b/src/workos/types/authorization/role_assignment.py @@ -4,13 +4,11 @@ class RoleAssignmentRole(WorkOSModel): - """The role associated with a role assignment.""" slug: str class RoleAssignmentResource(WorkOSModel): - """The resource associated with a role assignment.""" id: str external_id: str @@ -18,7 +16,6 @@ class RoleAssignmentResource(WorkOSModel): class RoleAssignment(WorkOSModel): - """Representation of a WorkOS Authorization Role Assignment.""" object: Literal["role_assignment"] id: str diff --git a/src/workos/utils/_base_http_client.py b/src/workos/utils/_base_http_client.py index e2164f1a..f467b371 100644 --- a/src/workos/utils/_base_http_client.py +++ b/src/workos/utils/_base_http_client.py @@ -135,7 +135,7 @@ def _prepare_request( params Optional[dict]: Query params or body payload to be added to the request headers Optional[dict]: Custom headers to be added to the request token Optional[str]: Bearer token - force_include_body (bool): If True, allows sending a body with DELETE requests + force_include_body (bool): If True, allows sending a body in a bodyless request (used for DELETE requests) Returns: dict: Response from WorkOS From 526eb7d61fa1c83bad2a6f55ade102944bbec81f Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Thu, 19 Feb 2026 14:35:55 -1000 Subject: [PATCH 04/42] lol --- .../authorization/organization_membership.py | 4 +- ...{authorization_resource.py => resource.py} | 5 +- .../types/authorization/role_assignment.py | 3 - src/workos/utils/_base_http_client.py | 2 +- src/workos/utils/http_client.py | 8 + tests/test_async_http_client.py | 37 +++++ tests/test_authorization_types.py | 144 ++++++++++++++++++ tests/test_sync_http_client.py | 33 ++++ tests/utils/fixtures/mock_resource.py | 25 +++ tests/utils/fixtures/mock_role_assignment.py | 31 ++++ 10 files changed, 285 insertions(+), 7 deletions(-) rename src/workos/types/authorization/{authorization_resource.py => resource.py} (72%) create mode 100644 tests/test_authorization_types.py create mode 100644 tests/utils/fixtures/mock_resource.py create mode 100644 tests/utils/fixtures/mock_role_assignment.py diff --git a/src/workos/types/authorization/organization_membership.py b/src/workos/types/authorization/organization_membership.py index 73d1983c..50d77bec 100644 --- a/src/workos/types/authorization/organization_membership.py +++ b/src/workos/types/authorization/organization_membership.py @@ -10,7 +10,9 @@ class AuthorizationOrganizationMembership(WorkOSModel): """Representation of an Organization Membership returned by Authorization endpoints. This is a separate type from the user_management OrganizationMembership because - authorization endpoints return memberships without the `role` field. + authorization endpoints return memberships without the ``role`` field and include + ``organization_name``. Additionally, ``custom_attributes`` is optional here as + authorization endpoints may omit it. """ object: Literal["organization_membership"] diff --git a/src/workos/types/authorization/authorization_resource.py b/src/workos/types/authorization/resource.py similarity index 72% rename from src/workos/types/authorization/authorization_resource.py rename to src/workos/types/authorization/resource.py index 769d93fd..917673c4 100644 --- a/src/workos/types/authorization/authorization_resource.py +++ b/src/workos/types/authorization/resource.py @@ -1,9 +1,10 @@ -from typing import Any, Literal, Mapping, Optional +from typing import Literal, Optional from workos.types.workos_model import WorkOSModel -class AuthorizationResource(WorkOSModel): +class Resource(WorkOSModel): + """Representation of an Authorization Resource.""" object: Literal["authorization_resource"] id: str diff --git a/src/workos/types/authorization/role_assignment.py b/src/workos/types/authorization/role_assignment.py index 5d0888de..9ca59936 100644 --- a/src/workos/types/authorization/role_assignment.py +++ b/src/workos/types/authorization/role_assignment.py @@ -4,19 +4,16 @@ class RoleAssignmentRole(WorkOSModel): - slug: str class RoleAssignmentResource(WorkOSModel): - id: str external_id: str resource_type_slug: str class RoleAssignment(WorkOSModel): - object: Literal["role_assignment"] id: str role: RoleAssignmentRole diff --git a/src/workos/utils/_base_http_client.py b/src/workos/utils/_base_http_client.py index f467b371..ad5ebaa5 100644 --- a/src/workos/utils/_base_http_client.py +++ b/src/workos/utils/_base_http_client.py @@ -134,7 +134,7 @@ def _prepare_request( method Optional[str]: One of the supported methods as defined by the REQUEST_METHOD_X constants params Optional[dict]: Query params or body payload to be added to the request headers Optional[dict]: Custom headers to be added to the request - token Optional[str]: Bearer token + exclude_default_auth_headers (bool): If True, excludes default auth headers from the request force_include_body (bool): If True, allows sending a body in a bodyless request (used for DELETE requests) Returns: diff --git a/src/workos/utils/http_client.py b/src/workos/utils/http_client.py index 0af5834c..9a2d7a57 100644 --- a/src/workos/utils/http_client.py +++ b/src/workos/utils/http_client.py @@ -117,14 +117,18 @@ def delete_with_body( self, path: str, json: JsonType = None, + params: ParamsType = None, headers: HeadersType = None, + exclude_default_auth_headers: bool = False, ) -> ResponseJson: """Executes a DELETE request with a JSON body against the WorkOS API.""" prepared_request_parameters = self._prepare_request( path=path, method=REQUEST_METHOD_DELETE, json=json, + params=params, headers=headers, + exclude_default_auth_headers=exclude_default_auth_headers, force_include_body=True, ) response = self._client.request(**prepared_request_parameters) @@ -231,14 +235,18 @@ async def delete_with_body( self, path: str, json: JsonType = None, + params: ParamsType = None, headers: HeadersType = None, + exclude_default_auth_headers: bool = False, ) -> ResponseJson: """Executes a DELETE request with a JSON body against the WorkOS API.""" prepared_request_parameters = self._prepare_request( path=path, method=REQUEST_METHOD_DELETE, json=json, + params=params, headers=headers, + exclude_default_auth_headers=exclude_default_auth_headers, force_include_body=True, ) response = await self._client.request(**prepared_request_parameters) diff --git a/tests/test_async_http_client.py b/tests/test_async_http_client.py index 633ed71a..c9da72ba 100644 --- a/tests/test_async_http_client.py +++ b/tests/test_async_http_client.py @@ -326,3 +326,40 @@ async def test_request_removes_none_json_values( json={"organization_id": None, "test": "value"}, ) assert request_kwargs["json"] == {"test": "value"} + + async def test_delete_with_body_sends_json( + self, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) + + await self.http_client.delete_with_body( + path="/test", + json={"resource_id": "res_01ABC"}, + ) + + assert request_kwargs["method"] == "delete" + assert request_kwargs["json"] == {"resource_id": "res_01ABC"} + + async def test_delete_with_body_sends_params( + self, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) + + await self.http_client.delete_with_body( + path="/test", + json={"resource_id": "res_01ABC"}, + params={"org_id": "org_01ABC"}, + ) + + assert request_kwargs["params"] == {"org_id": "org_01ABC"} + assert request_kwargs["json"] == {"resource_id": "res_01ABC"} + + async def test_delete_without_body_raises_value_error(self): + with pytest.raises( + ValueError, match="Cannot send a body with a delete request" + ): + await self.http_client.request( + path="/test", + method="delete", + json={"should": "fail"}, + ) diff --git a/tests/test_authorization_types.py b/tests/test_authorization_types.py new file mode 100644 index 00000000..a3480bb5 --- /dev/null +++ b/tests/test_authorization_types.py @@ -0,0 +1,144 @@ +"""Tests for new authorization types: Resource, RoleAssignment, AccessEvaluation, +AuthorizationOrganizationMembership.""" + +from workos.types.authorization import ( + AccessEvaluation, + AuthorizationOrganizationMembership, + Resource, + RoleAssignment, + RoleAssignmentResource, + RoleAssignmentRole, +) + + +class TestAccessEvaluation: + def test_authorized_true(self): + result = AccessEvaluation(authorized=True) + assert result.authorized is True + + def test_authorized_false(self): + result = AccessEvaluation(authorized=False) + assert result.authorized is False + + def test_from_dict(self): + result = AccessEvaluation.model_validate({"authorized": True}) + assert result.authorized is True + + +class TestResource: + def test_resource_deserialization(self): + data = { + "object": "authorization_resource", + "id": "res_01ABC", + "external_id": "ext_123", + "name": "Test Document", + "resource_type_slug": "document", + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + } + resource = Resource.model_validate(data) + + assert resource.object == "authorization_resource" + assert resource.id == "res_01ABC" + assert resource.external_id == "ext_123" + assert resource.name == "Test Document" + assert resource.resource_type_slug == "document" + assert resource.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" + assert resource.description is None + assert resource.parent_resource_id is None + + def test_resource_with_optional_fields(self): + data = { + "object": "authorization_resource", + "id": "res_01ABC", + "external_id": "ext_123", + "name": "Test Document", + "description": "A test document resource", + "resource_type_slug": "document", + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "parent_resource_id": "res_01PARENT", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + } + resource = Resource.model_validate(data) + + assert resource.description == "A test document resource" + assert resource.parent_resource_id == "res_01PARENT" + + +class TestRoleAssignment: + def test_role_assignment_deserialization(self): + data = { + "object": "role_assignment", + "id": "ra_01ABC", + "role": {"slug": "admin"}, + "resource": { + "id": "res_01ABC", + "external_id": "ext_123", + "resource_type_slug": "document", + }, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + } + assignment = RoleAssignment.model_validate(data) + + assert assignment.object == "role_assignment" + assert assignment.id == "ra_01ABC" + assert assignment.role.slug == "admin" + assert assignment.resource.id == "res_01ABC" + assert assignment.resource.external_id == "ext_123" + assert assignment.resource.resource_type_slug == "document" + + def test_role_assignment_role(self): + role = RoleAssignmentRole(slug="editor") + assert role.slug == "editor" + + def test_role_assignment_resource(self): + resource = RoleAssignmentResource( + id="res_01ABC", + external_id="ext_123", + resource_type_slug="document", + ) + assert resource.id == "res_01ABC" + assert resource.external_id == "ext_123" + assert resource.resource_type_slug == "document" + + +class TestAuthorizationOrganizationMembership: + def test_membership_deserialization(self): + data = { + "object": "organization_membership", + "id": "om_01ABC", + "user_id": "user_01ABC", + "organization_id": "org_01ABC", + "organization_name": "Test Org", + "status": "active", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + } + membership = AuthorizationOrganizationMembership.model_validate(data) + + assert membership.object == "organization_membership" + assert membership.id == "om_01ABC" + assert membership.user_id == "user_01ABC" + assert membership.organization_id == "org_01ABC" + assert membership.organization_name == "Test Org" + assert membership.status == "active" + assert membership.custom_attributes is None + + def test_membership_with_custom_attributes(self): + data = { + "object": "organization_membership", + "id": "om_01ABC", + "user_id": "user_01ABC", + "organization_id": "org_01ABC", + "organization_name": "Test Org", + "status": "active", + "custom_attributes": {"department": "Engineering"}, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + } + membership = AuthorizationOrganizationMembership.model_validate(data) + + assert membership.custom_attributes == {"department": "Engineering"} diff --git a/tests/test_sync_http_client.py b/tests/test_sync_http_client.py index edbba0b4..c75ae504 100644 --- a/tests/test_sync_http_client.py +++ b/tests/test_sync_http_client.py @@ -372,3 +372,36 @@ def test_request_removes_none_json_values( json={"organization_id": None, "test": "value"}, ) assert request_kwargs["json"] == {"test": "value"} + + def test_delete_with_body_sends_json(self, capture_and_mock_http_client_request): + request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) + + self.http_client.delete_with_body( + path="/test", + json={"resource_id": "res_01ABC"}, + ) + + assert request_kwargs["method"] == "delete" + assert request_kwargs["json"] == {"resource_id": "res_01ABC"} + + def test_delete_with_body_sends_params(self, capture_and_mock_http_client_request): + request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) + + self.http_client.delete_with_body( + path="/test", + json={"resource_id": "res_01ABC"}, + params={"org_id": "org_01ABC"}, + ) + + assert request_kwargs["params"] == {"org_id": "org_01ABC"} + assert request_kwargs["json"] == {"resource_id": "res_01ABC"} + + def test_delete_without_body_raises_value_error(self): + with pytest.raises( + ValueError, match="Cannot send a body with a delete request" + ): + self.http_client.request( + path="/test", + method="delete", + json={"should": "fail"}, + ) diff --git a/tests/utils/fixtures/mock_resource.py b/tests/utils/fixtures/mock_resource.py new file mode 100644 index 00000000..825bf5fb --- /dev/null +++ b/tests/utils/fixtures/mock_resource.py @@ -0,0 +1,25 @@ +import datetime + +from workos.types.authorization.resource import Resource + + +class MockResource(Resource): + def __init__( + self, + id: str = "res_01ABC", + external_id: str = "ext_123", + name: str = "Test Resource", + resource_type_slug: str = "document", + organization_id: str = "org_01EHT88Z8J8795GZNQ4ZP1J81T", + ): + now = datetime.datetime.now().isoformat() + super().__init__( + object="authorization_resource", + id=id, + external_id=external_id, + name=name, + resource_type_slug=resource_type_slug, + organization_id=organization_id, + created_at=now, + updated_at=now, + ) diff --git a/tests/utils/fixtures/mock_role_assignment.py b/tests/utils/fixtures/mock_role_assignment.py new file mode 100644 index 00000000..23b2dcde --- /dev/null +++ b/tests/utils/fixtures/mock_role_assignment.py @@ -0,0 +1,31 @@ +import datetime + +from workos.types.authorization.role_assignment import ( + RoleAssignment, + RoleAssignmentResource, + RoleAssignmentRole, +) + + +class MockRoleAssignment(RoleAssignment): + def __init__( + self, + id: str = "ra_01ABC", + role_slug: str = "admin", + resource_id: str = "res_01ABC", + resource_external_id: str = "ext_123", + resource_type_slug: str = "document", + ): + now = datetime.datetime.now().isoformat() + super().__init__( + object="role_assignment", + id=id, + role=RoleAssignmentRole(slug=role_slug), + resource=RoleAssignmentResource( + id=resource_id, + external_id=resource_external_id, + resource_type_slug=resource_type_slug, + ), + created_at=now, + updated_at=now, + ) From 13dad51e538af0ac7d0a8f5adfe23189da3f6ea4 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 23 Feb 2026 04:38:28 -1000 Subject: [PATCH 05/42] nits for re-dupe OrganizationMembershipStatus --- src/workos/types/authorization/organization_membership.py | 3 +-- src/workos/types/user_management/list_filters.py | 5 +---- src/workos/types/user_management/organization_membership.py | 4 +--- .../types/user_management/organization_membership_status.py | 3 +++ 4 files changed, 6 insertions(+), 9 deletions(-) create mode 100644 src/workos/types/user_management/organization_membership_status.py diff --git a/src/workos/types/authorization/organization_membership.py b/src/workos/types/authorization/organization_membership.py index 50d77bec..122fc55c 100644 --- a/src/workos/types/authorization/organization_membership.py +++ b/src/workos/types/authorization/organization_membership.py @@ -1,10 +1,9 @@ from typing import Any, Literal, Mapping, Optional +from workos.types.user_management.organization_membership_status import OrganizationMembershipStatus from workos.types.workos_model import WorkOSModel from workos.typing.literals import LiteralOrUntyped -OrganizationMembershipStatus = Literal["active", "inactive", "pending"] - class AuthorizationOrganizationMembership(WorkOSModel): """Representation of an Organization Membership returned by Authorization endpoints. diff --git a/src/workos/types/user_management/list_filters.py b/src/workos/types/user_management/list_filters.py index a3be45ce..344ea62f 100644 --- a/src/workos/types/user_management/list_filters.py +++ b/src/workos/types/user_management/list_filters.py @@ -1,9 +1,6 @@ from typing import Optional, Sequence from workos.types.list_resource import ListArgs -from workos.types.user_management.organization_membership import ( - OrganizationMembershipStatus, -) - +from workos.types.user_management.organization_membership_status import OrganizationMembershipStatus class UsersListFilters(ListArgs, total=False): email: Optional[str] diff --git a/src/workos/types/user_management/organization_membership.py b/src/workos/types/user_management/organization_membership.py index 5c7bda0f..c803c113 100644 --- a/src/workos/types/user_management/organization_membership.py +++ b/src/workos/types/user_management/organization_membership.py @@ -1,12 +1,10 @@ from typing import Any, Literal, Mapping, Optional, Sequence from typing_extensions import TypedDict +from workos.types.user_management.organization_membership_status import OrganizationMembershipStatus from workos.types.workos_model import WorkOSModel from workos.typing.literals import LiteralOrUntyped -OrganizationMembershipStatus = Literal["active", "inactive", "pending"] - - class OrganizationMembershipRole(TypedDict): slug: str diff --git a/src/workos/types/user_management/organization_membership_status.py b/src/workos/types/user_management/organization_membership_status.py new file mode 100644 index 00000000..c79384cf --- /dev/null +++ b/src/workos/types/user_management/organization_membership_status.py @@ -0,0 +1,3 @@ +from typing import Literal + +OrganizationMembershipStatus = Literal["active", "inactive", "pending"] From ff239c73b3008e5962cd6ed710a53b1539e0d1e5 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 23 Feb 2026 04:46:37 -1000 Subject: [PATCH 06/42] Format --- src/workos/types/authorization/organization_membership.py | 4 +++- src/workos/types/user_management/list_filters.py | 5 ++++- src/workos/types/user_management/organization_membership.py | 5 ++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/workos/types/authorization/organization_membership.py b/src/workos/types/authorization/organization_membership.py index 122fc55c..a5390bb0 100644 --- a/src/workos/types/authorization/organization_membership.py +++ b/src/workos/types/authorization/organization_membership.py @@ -1,6 +1,8 @@ from typing import Any, Literal, Mapping, Optional -from workos.types.user_management.organization_membership_status import OrganizationMembershipStatus +from workos.types.user_management.organization_membership_status import ( + OrganizationMembershipStatus, +) from workos.types.workos_model import WorkOSModel from workos.typing.literals import LiteralOrUntyped diff --git a/src/workos/types/user_management/list_filters.py b/src/workos/types/user_management/list_filters.py index 344ea62f..99d92905 100644 --- a/src/workos/types/user_management/list_filters.py +++ b/src/workos/types/user_management/list_filters.py @@ -1,6 +1,9 @@ from typing import Optional, Sequence from workos.types.list_resource import ListArgs -from workos.types.user_management.organization_membership_status import OrganizationMembershipStatus +from workos.types.user_management.organization_membership_status import ( + OrganizationMembershipStatus, +) + class UsersListFilters(ListArgs, total=False): email: Optional[str] diff --git a/src/workos/types/user_management/organization_membership.py b/src/workos/types/user_management/organization_membership.py index c803c113..df9e5acf 100644 --- a/src/workos/types/user_management/organization_membership.py +++ b/src/workos/types/user_management/organization_membership.py @@ -1,10 +1,13 @@ from typing import Any, Literal, Mapping, Optional, Sequence from typing_extensions import TypedDict -from workos.types.user_management.organization_membership_status import OrganizationMembershipStatus +from workos.types.user_management.organization_membership_status import ( + OrganizationMembershipStatus, +) from workos.types.workos_model import WorkOSModel from workos.typing.literals import LiteralOrUntyped + class OrganizationMembershipRole(TypedDict): slug: str From a5823c6df881fc62ca00d7372a6c14bd6adc4e9f Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 23 Feb 2026 04:49:36 -1000 Subject: [PATCH 07/42] adding AUTHORIZATION_RESOURCES_PATH to base --- src/workos/authorization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 6e12f035..ffa890a3 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -28,6 +28,7 @@ ) AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions" +AUTHORIZATION_RESOURCES_PATH = "authorization/resources" _role_adapter: TypeAdapter[Role] = TypeAdapter(Role) From 8dd434f540abe03579b230f2ca3cf1123d54098f Mon Sep 17 00:00:00 2001 From: swaroopThereItIs Date: Tue, 24 Feb 2026 10:31:07 -1000 Subject: [PATCH 08/42] FGA_1: create/delete/get/update resource (#563) --- src/workos/authorization.py | 210 +++++++++++++++++- src/workos/utils/_base_http_client.py | 3 +- src/workos/utils/http_client.py | 6 + tests/test_authorization_resource_crud.py | 257 ++++++++++++++++++++++ 4 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 tests/test_authorization_resource_crud.py diff --git a/src/workos/authorization.py b/src/workos/authorization.py index ffa890a3..1d3ed1b9 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -1,6 +1,8 @@ -from typing import Any, Dict, Optional, Protocol, Sequence +from enum import Enum +from typing import Any, Dict, Optional, Protocol, Sequence, Union from pydantic import TypeAdapter +from typing_extensions import TypedDict from workos.types.authorization.environment_role import ( EnvironmentRole, @@ -8,6 +10,7 @@ ) from workos.types.authorization.organization_role import OrganizationRole from workos.types.authorization.permission import Permission +from workos.types.authorization.resource import Resource from workos.types.authorization.role import Role, RoleList from workos.types.list_resource import ( ListArgs, @@ -27,9 +30,28 @@ REQUEST_METHOD_PUT, ) + +class _Unset(Enum): + TOKEN = 0 + + +UNSET: _Unset = _Unset.TOKEN + AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions" AUTHORIZATION_RESOURCES_PATH = "authorization/resources" + +class ParentResourceById(TypedDict): + parent_resource_id: str + + +class ParentResourceByExternalId(TypedDict): + parent_resource_external_id: str + parent_resource_type_slug: str + + +ParentResource = Union[ParentResourceById, ParentResourceByExternalId] + _role_adapter: TypeAdapter[Role] = TypeAdapter(Role) @@ -162,6 +184,36 @@ def add_environment_role_permission( permission_slug: str, ) -> SyncOrAsync[EnvironmentRole]: ... + # Resources + + def get_resource(self, resource_id: str) -> SyncOrAsync[Resource]: ... + + def create_resource( + self, + *, + resource_type_slug: str, + organization_id: str, + external_id: str, + name: str, + parent: Optional[ParentResource] = None, + description: Optional[str] = None, + ) -> SyncOrAsync[Resource]: ... + + def update_resource( + self, + resource_id: str, + *, + name: Optional[str] = None, + description: Union[str, None, _Unset] = UNSET, + ) -> SyncOrAsync[Resource]: ... + + def delete_resource( + self, + resource_id: str, + *, + cascade_delete: Optional[bool] = None, + ) -> SyncOrAsync[None]: ... + class Authorization(AuthorizationModule): _http_client: SyncHTTPClient @@ -438,6 +490,84 @@ def add_environment_role_permission( return EnvironmentRole.model_validate(response) + # Resources + + def get_resource(self, resource_id: str) -> Resource: + response = self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_GET, + ) + + return Resource.model_validate(response) + + def create_resource( + self, + *, + resource_type_slug: str, + organization_id: str, + external_id: str, + name: str, + parent: Optional[ParentResource] = None, + description: Optional[str] = None, + ) -> Resource: + json: Dict[str, Any] = { + "resource_type_slug": resource_type_slug, + "organization_id": organization_id, + "external_id": external_id, + "name": name, + } + if parent is not None: + json.update(parent) + if description is not None: + json["description"] = description + + response = self._http_client.request( + AUTHORIZATION_RESOURCES_PATH, + method=REQUEST_METHOD_POST, + json=json, + ) + + return Resource.model_validate(response) + + def update_resource( + self, + resource_id: str, + *, + name: Optional[str] = None, + description: Union[str, None, _Unset] = UNSET, + ) -> Resource: + json: Dict[str, Any] = {} + if name is not None: + json["name"] = name + if not isinstance(description, _Unset): + json["description"] = description + + response = self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_PATCH, + json=json, + exclude_none=False, + ) + + return Resource.model_validate(response) + + def delete_resource( + self, + resource_id: str, + *, + cascade_delete: Optional[bool] = None, + ) -> None: + if cascade_delete is not None: + self._http_client.delete_with_body( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + json={"cascade_delete": cascade_delete}, + ) + else: + self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_DELETE, + ) + class AsyncAuthorization(AuthorizationModule): _http_client: AsyncHTTPClient @@ -713,3 +843,81 @@ async def add_environment_role_permission( ) return EnvironmentRole.model_validate(response) + + # Resources + + async def get_resource(self, resource_id: str) -> Resource: + response = await self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_GET, + ) + + return Resource.model_validate(response) + + async def create_resource( + self, + *, + resource_type_slug: str, + organization_id: str, + external_id: str, + name: str, + parent: Optional[ParentResource] = None, + description: Optional[str] = None, + ) -> Resource: + json: Dict[str, Any] = { + "resource_type_slug": resource_type_slug, + "organization_id": organization_id, + "external_id": external_id, + "name": name, + } + if parent is not None: + json.update(parent) + if description is not None: + json["description"] = description + + response = await self._http_client.request( + AUTHORIZATION_RESOURCES_PATH, + method=REQUEST_METHOD_POST, + json=json, + ) + + return Resource.model_validate(response) + + async def update_resource( + self, + resource_id: str, + *, + name: Optional[str] = None, + description: Union[str, None, _Unset] = UNSET, + ) -> Resource: + json: Dict[str, Any] = {} + if name is not None: + json["name"] = name + if not isinstance(description, _Unset): + json["description"] = description + + response = await self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_PATCH, + json=json, + exclude_none=False, + ) + + return Resource.model_validate(response) + + async def delete_resource( + self, + resource_id: str, + *, + cascade_delete: Optional[bool] = None, + ) -> None: + if cascade_delete is not None: + await self._http_client.delete_with_body( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + json={"cascade_delete": cascade_delete}, + ) + else: + await self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_DELETE, + ) diff --git a/src/workos/utils/_base_http_client.py b/src/workos/utils/_base_http_client.py index ad5ebaa5..402d71c2 100644 --- a/src/workos/utils/_base_http_client.py +++ b/src/workos/utils/_base_http_client.py @@ -124,6 +124,7 @@ def _prepare_request( headers: HeadersType = None, exclude_default_auth_headers: bool = False, force_include_body: bool = False, + exclude_none: bool = True, ) -> PreparedRequest: """Executes a request against the WorkOS API. @@ -159,7 +160,7 @@ def _prepare_request( params = {k: v for k, v in params.items() if v is not None} # Remove any body values that are None - if json is not None and isinstance(json, Mapping): + if exclude_none and json is not None and isinstance(json, Mapping): json = {k: v for k, v in json.items() if v is not None} # We'll spread these return values onto the HTTP client request method diff --git a/src/workos/utils/http_client.py b/src/workos/utils/http_client.py index 9a2d7a57..5c7deac5 100644 --- a/src/workos/utils/http_client.py +++ b/src/workos/utils/http_client.py @@ -88,6 +88,7 @@ def request( json: JsonType = None, headers: HeadersType = None, exclude_default_auth_headers: bool = False, + exclude_none: bool = True, ) -> ResponseJson: """Executes a request against the WorkOS API. @@ -98,6 +99,7 @@ def request( method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants params (ParamsType): Query params to be added to the request json (JsonType): Body payload to be added to the request + exclude_none (bool): If True, removes None values from the JSON body Returns: ResponseJson: Response from WorkOS @@ -109,6 +111,7 @@ def request( json=json, headers=headers, exclude_default_auth_headers=exclude_default_auth_headers, + exclude_none=exclude_none, ) response = self._client.request(**prepared_request_parameters) return self._handle_response(response) @@ -206,6 +209,7 @@ async def request( json: JsonType = None, headers: HeadersType = None, exclude_default_auth_headers: bool = False, + exclude_none: bool = True, ) -> ResponseJson: """Executes a request against the WorkOS API. @@ -216,6 +220,7 @@ async def request( method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants params (ParamsType): Query params to be added to the request json (JsonType): Body payload to be added to the request + exclude_none (bool): If True, removes None values from the JSON body Returns: ResponseJson: Response from WorkOS @@ -227,6 +232,7 @@ async def request( json=json, headers=headers, exclude_default_auth_headers=exclude_default_auth_headers, + exclude_none=exclude_none, ) response = await self._client.request(**prepared_request_parameters) return self._handle_response(response) diff --git a/tests/test_authorization_resource_crud.py b/tests/test_authorization_resource_crud.py new file mode 100644 index 00000000..3ff79a2d --- /dev/null +++ b/tests/test_authorization_resource_crud.py @@ -0,0 +1,257 @@ +from typing import Union + +import pytest +from tests.utils.fixtures.mock_resource import MockResource +from tests.utils.syncify import syncify +from workos.authorization import AsyncAuthorization, Authorization + + +@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) +class TestAuthorizationResourceCRUD: + @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_resource(self): + return MockResource(id="res_01ABC").dict() + + # --- get_resource --- + + def test_get_resource(self, mock_resource, capture_and_mock_http_client_request): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + resource = syncify(self.authorization.get_resource("res_01ABC")) + + assert resource.id == "res_01ABC" + assert resource.object == "authorization_resource" + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + + # --- create_resource --- + + def test_create_resource_required_fields_only( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 201 + ) + + resource = syncify( + self.authorization.create_resource( + resource_type_slug="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + external_id="ext_123", + name="Test Resource", + parent={"parent_resource_id": "res_01PARENT"}, + ) + ) + + assert resource.id == "res_01ABC" + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith("/authorization/resources") + assert request_kwargs["json"] == { + "resource_type_slug": "document", + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "external_id": "ext_123", + "name": "Test Resource", + "parent_resource_id": "res_01PARENT", + } + + def test_create_resource_without_parent( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 201 + ) + + resource = syncify( + self.authorization.create_resource( + resource_type_slug="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + external_id="ext_123", + name="Test Resource", + ) + ) + + assert resource.id == "res_01ABC" + assert request_kwargs["method"] == "post" + assert request_kwargs["json"] == { + "resource_type_slug": "document", + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "external_id": "ext_123", + "name": "Test Resource", + } + + def test_create_resource_with_all_optional_fields( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 201 + ) + + syncify( + self.authorization.create_resource( + resource_type_slug="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + external_id="ext_123", + name="Test Resource", + parent={"parent_resource_id": "res_01PARENT"}, + description="A test document", + ) + ) + + assert request_kwargs["json"] == { + "resource_type_slug": "document", + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "external_id": "ext_123", + "name": "Test Resource", + "parent_resource_id": "res_01PARENT", + "description": "A test document", + } + + def test_create_resource_with_parent_by_id( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 201 + ) + + syncify( + self.authorization.create_resource( + resource_type_slug="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + external_id="ext_123", + name="Test Resource", + parent={"parent_resource_id": "res_01PARENT"}, + ) + ) + + assert request_kwargs["json"]["parent_resource_id"] == "res_01PARENT" + + def test_create_resource_with_parent_by_external_id( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 201 + ) + + syncify( + self.authorization.create_resource( + resource_type_slug="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + external_id="ext_123", + name="Test Resource", + parent={ + "parent_resource_external_id": "ext_parent_456", + "parent_resource_type_slug": "folder", + }, + ) + ) + + assert request_kwargs["json"]["parent_resource_external_id"] == "ext_parent_456" + assert request_kwargs["json"]["parent_resource_type_slug"] == "folder" + + # --- update_resource --- + + def test_update_resource_with_meta( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + resource = syncify( + self.authorization.update_resource( + "res_01ABC", + name="Updated Name", + description="Updated description", + ) + ) + + assert resource.id == "res_01ABC" + assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + assert request_kwargs["json"] == { + "name": "Updated Name", + "description": "Updated description", + } + + def test_update_resource_clear_description( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + syncify(self.authorization.update_resource("res_01ABC", description=None)) + + assert request_kwargs["method"] == "patch" + assert request_kwargs["json"] == {"description": None} + + def test_update_resource_without_meta( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + syncify(self.authorization.update_resource("res_01ABC")) + + assert request_kwargs["method"] == "patch" + assert request_kwargs["json"] == {} + + def test_update_resource_without_desc( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + resource = syncify( + self.authorization.update_resource( + "res_01ABC", + name="Updated Name", + ) + ) + + assert resource.id == "res_01ABC" + assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + assert request_kwargs["json"] == {"name": "Updated Name"} + + # --- delete_resource --- + + def test_delete_resource_without_cascade( + self, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, + status_code=202, + headers={"content-type": "text/plain; charset=utf-8"}, + ) + + response = syncify(self.authorization.delete_resource("res_01ABC")) + + assert response is None + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + + def test_delete_resource_with_cascade(self, capture_and_mock_http_client_request): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, + status_code=202, + headers={"content-type": "text/plain; charset=utf-8"}, + ) + + response = syncify( + self.authorization.delete_resource("res_01ABC", cascade_delete=True) + ) + + assert response is None + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + assert request_kwargs["json"] == {"cascade_delete": True} From fe59018ef0b9664869bfdfb64d1a4d65684aeb33 Mon Sep 17 00:00:00 2001 From: swaroopThereItIs Date: Tue, 24 Feb 2026 10:50:50 -1000 Subject: [PATCH 09/42] FGA_2: listResources(), get/update/delete resource_by_external_id (#569) --- src/workos/authorization.py | 254 ++++++++++++++++ ...test_authorization_resource_external_id.py | 272 ++++++++++++++++++ 2 files changed, 526 insertions(+) create mode 100644 tests/test_authorization_resource_external_id.py diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 1d3ed1b9..3d545054 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -39,6 +39,19 @@ class _Unset(Enum): AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions" AUTHORIZATION_RESOURCES_PATH = "authorization/resources" +AUTHORIZATION_ORGANIZATIONS_PATH = "authorization/organizations" + + +class ResourceListFilters(ListArgs, total=False): + organization_id: Optional[str] + resource_type_slug: Optional[str] + parent_resource_id: Optional[str] + parent_resource_type_slug: Optional[str] + parent_external_id: Optional[str] + search: Optional[str] + + +ResourcesListResource = WorkOSListResource[Resource, ResourceListFilters, ListMetadata] class ParentResourceById(TypedDict): @@ -214,6 +227,47 @@ def delete_resource( cascade_delete: Optional[bool] = None, ) -> SyncOrAsync[None]: ... + def list_resources( + self, + *, + organization_id: Optional[str] = None, + resource_type_slug: Optional[str] = None, + parent_resource_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + parent_external_id: Optional[str] = None, + search: Optional[str] = None, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> SyncOrAsync[ResourcesListResource]: ... + + def get_resource_by_external_id( + self, + organization_id: str, + resource_type: str, + external_id: str, + ) -> SyncOrAsync[Resource]: ... + + def update_resource_by_external_id( + self, + organization_id: str, + resource_type: str, + external_id: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> SyncOrAsync[Resource]: ... + + def delete_resource_by_external_id( + self, + organization_id: str, + resource_type: str, + external_id: str, + *, + cascade_delete: Optional[bool] = None, + ) -> SyncOrAsync[None]: ... + class Authorization(AuthorizationModule): _http_client: SyncHTTPClient @@ -568,6 +622,106 @@ def delete_resource( method=REQUEST_METHOD_DELETE, ) + def list_resources( + self, + *, + organization_id: Optional[str] = None, + resource_type_slug: Optional[str] = None, + parent_resource_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + parent_external_id: Optional[str] = None, + search: Optional[str] = None, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> ResourcesListResource: + list_params: ResourceListFilters = { + "limit": limit, + "before": before, + "after": after, + "order": order, + } + if organization_id is not None: + list_params["organization_id"] = organization_id + if resource_type_slug is not None: + list_params["resource_type_slug"] = resource_type_slug + if parent_resource_id is not None: + list_params["parent_resource_id"] = parent_resource_id + if parent_resource_type_slug is not None: + list_params["parent_resource_type_slug"] = parent_resource_type_slug + if parent_external_id is not None: + list_params["parent_external_id"] = parent_external_id + if search is not None: + list_params["search"] = search + + response = self._http_client.request( + AUTHORIZATION_RESOURCES_PATH, + method=REQUEST_METHOD_GET, + params=list_params, + ) + + return WorkOSListResource[Resource, ResourceListFilters, ListMetadata]( + list_method=self.list_resources, + list_args=list_params, + **ListPage[Resource](**response).model_dump(), + ) + + def get_resource_by_external_id( + self, + organization_id: str, + resource_type: str, + external_id: str, + ) -> Resource: + response = self._http_client.request( + f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}", + method=REQUEST_METHOD_GET, + ) + + return Resource.model_validate(response) + + def update_resource_by_external_id( + self, + organization_id: str, + resource_type: str, + external_id: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> Resource: + json: Dict[str, Any] = {} + if name is not None: + json["name"] = name + if description is not None: + json["description"] = description + + response = self._http_client.request( + f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}", + method=REQUEST_METHOD_PATCH, + json=json, + ) + + return Resource.model_validate(response) + + def delete_resource_by_external_id( + self, + organization_id: str, + resource_type: str, + external_id: str, + *, + cascade_delete: Optional[bool] = None, + ) -> None: + path = f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}" + params: Dict[str, bool] = {} + if cascade_delete is not None: + params["cascade_delete"] = cascade_delete + + self._http_client.request( + path, + method=REQUEST_METHOD_DELETE, + params=params if params else None, + ) + class AsyncAuthorization(AuthorizationModule): _http_client: AsyncHTTPClient @@ -921,3 +1075,103 @@ async def delete_resource( f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", method=REQUEST_METHOD_DELETE, ) + + async def list_resources( + self, + *, + organization_id: Optional[str] = None, + resource_type_slug: Optional[str] = None, + parent_resource_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + parent_external_id: Optional[str] = None, + search: Optional[str] = None, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> ResourcesListResource: + list_params: ResourceListFilters = { + "limit": limit, + "before": before, + "after": after, + "order": order, + } + if organization_id is not None: + list_params["organization_id"] = organization_id + if resource_type_slug is not None: + list_params["resource_type_slug"] = resource_type_slug + if parent_resource_id is not None: + list_params["parent_resource_id"] = parent_resource_id + if parent_resource_type_slug is not None: + list_params["parent_resource_type_slug"] = parent_resource_type_slug + if parent_external_id is not None: + list_params["parent_external_id"] = parent_external_id + if search is not None: + list_params["search"] = search + + response = await self._http_client.request( + AUTHORIZATION_RESOURCES_PATH, + method=REQUEST_METHOD_GET, + params=list_params, + ) + + return WorkOSListResource[Resource, ResourceListFilters, ListMetadata]( + list_method=self.list_resources, + list_args=list_params, + **ListPage[Resource](**response).model_dump(), + ) + + async def get_resource_by_external_id( + self, + organization_id: str, + resource_type: str, + external_id: str, + ) -> Resource: + response = await self._http_client.request( + f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}", + method=REQUEST_METHOD_GET, + ) + + return Resource.model_validate(response) + + async def update_resource_by_external_id( + self, + organization_id: str, + resource_type: str, + external_id: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> Resource: + json: Dict[str, Any] = {} + if name is not None: + json["name"] = name + if description is not None: + json["description"] = description + + response = await self._http_client.request( + f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}", + method=REQUEST_METHOD_PATCH, + json=json, + ) + + return Resource.model_validate(response) + + async def delete_resource_by_external_id( + self, + organization_id: str, + resource_type: str, + external_id: str, + *, + cascade_delete: Optional[bool] = None, + ) -> None: + path = f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}" + params: Dict[str, bool] = {} + if cascade_delete is not None: + params["cascade_delete"] = cascade_delete + + await self._http_client.request( + path, + method=REQUEST_METHOD_DELETE, + params=params if params else None, + ) diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py new file mode 100644 index 00000000..10eb6b82 --- /dev/null +++ b/tests/test_authorization_resource_external_id.py @@ -0,0 +1,272 @@ +from typing import Union + +import pytest +from tests.utils.fixtures.mock_resource import MockResource +from tests.utils.list_resource import list_response_of +from tests.utils.syncify import syncify +from tests.types.test_auto_pagination_function import TestAutoPaginationFunction +from workos.authorization import AsyncAuthorization, Authorization + + +MOCK_ORG_ID = "org_01EHT88Z8J8795GZNQ4ZP1J81T" +MOCK_RESOURCE_TYPE = "document" +MOCK_EXTERNAL_ID = "ext_123" + + +@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) +class TestAuthorizationResourceExternalId: + @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_resource(self): + return MockResource( + id="res_01ABC", + external_id=MOCK_EXTERNAL_ID, + resource_type_slug=MOCK_RESOURCE_TYPE, + organization_id=MOCK_ORG_ID, + ).dict() + + @pytest.fixture + def mock_resources_list(self, mock_resource): + return list_response_of(data=[mock_resource]) + + @pytest.fixture + def mock_resources_empty_list(self): + return list_response_of(data=[]) + + @pytest.fixture + def mock_resources_multiple(self): + resources = [ + MockResource(id=f"res_{i:05d}", external_id=f"ext_{i}").dict() + for i in range(15) + ] + return resources + + # --- get_resource_by_external_id --- + + def test_get_resource_by_external_id( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + resource = syncify( + self.authorization.get_resource_by_external_id( + MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID + ) + ) + + assert resource.id == "res_01ABC" + assert resource.external_id == MOCK_EXTERNAL_ID + assert resource.object == "authorization_resource" + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith( + f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" + ) + + def test_get_resource_by_external_id_url_construction( + self, mock_resource, capture_and_mock_http_client_request + ): + org_id = "org_different" + res_type = "folder" + ext_id = "my-folder-123" + + mock_res = MockResource( + id="res_02XYZ", + external_id=ext_id, + resource_type_slug=res_type, + organization_id=org_id, + ).dict() + + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_res, 200 + ) + + resource = syncify( + self.authorization.get_resource_by_external_id(org_id, res_type, ext_id) + ) + + assert resource.id == "res_02XYZ" + assert request_kwargs["url"].endswith( + f"/authorization/organizations/{org_id}/resources/{res_type}/{ext_id}" + ) + + # --- update_resource_by_external_id --- + + def test_update_resource_by_external_id_with_name( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + resource = syncify( + self.authorization.update_resource_by_external_id( + MOCK_ORG_ID, + MOCK_RESOURCE_TYPE, + MOCK_EXTERNAL_ID, + name="Updated Name", + description="Updated description", + ) + ) + + assert resource.id == "res_01ABC" + assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith( + f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" + ) + assert request_kwargs["json"] == { + "name": "Updated Name", + "description": "Updated description", + } + + def test_update_resource_by_external_id_empty( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + syncify( + self.authorization.update_resource_by_external_id( + MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID + ) + ) + + assert request_kwargs["method"] == "patch" + assert request_kwargs["json"] == {} + + # --- delete_resource_by_external_id --- + + def test_delete_resource_by_external_id_without_cascade( + self, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, + status_code=202, + headers={"content-type": "text/plain; charset=utf-8"}, + ) + + response = syncify( + self.authorization.delete_resource_by_external_id( + MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID + ) + ) + + assert response is None + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith( + f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" + ) + + def test_delete_resource_by_external_id_with_cascade( + self, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, + status_code=202, + headers={"content-type": "text/plain; charset=utf-8"}, + ) + + response = syncify( + self.authorization.delete_resource_by_external_id( + MOCK_ORG_ID, + MOCK_RESOURCE_TYPE, + MOCK_EXTERNAL_ID, + cascade_delete=True, + ) + ) + + assert response is None + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith( + f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" + ) + assert request_kwargs["params"] == {"cascade_delete": True} + + # --- list_resources --- + + def test_list_resources_with_results( + self, mock_resources_list, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + resources_response = syncify( + self.authorization.list_resources(organization_id=MOCK_ORG_ID) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources") + assert request_kwargs["params"]["organization_id"] == MOCK_ORG_ID + assert len(resources_response.data) == 1 + assert resources_response.data[0].id == "res_01ABC" + + def test_list_resources_empty_results( + self, mock_resources_empty_list, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_empty_list, 200 + ) + + resources_response = syncify( + self.authorization.list_resources(organization_id=MOCK_ORG_ID) + ) + + assert request_kwargs["method"] == "get" + assert len(resources_response.data) == 0 + + def test_list_resources_with_resource_type_slug_filter( + self, mock_resources_list, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources( + organization_id=MOCK_ORG_ID, resource_type_slug="document" + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["params"]["resource_type_slug"] == "document" + + def test_list_resources_with_pagination_params( + self, mock_resources_list, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources( + organization_id=MOCK_ORG_ID, + limit=5, + after="res_cursor_abc", + before="res_cursor_xyz", + order="asc", + ) + ) + + assert request_kwargs["params"]["limit"] == 5 + assert request_kwargs["params"]["after"] == "res_cursor_abc" + assert request_kwargs["params"]["before"] == "res_cursor_xyz" + assert request_kwargs["params"]["order"] == "asc" + + def test_list_resources_auto_pagination( + self, + mock_resources_multiple, + test_auto_pagination: TestAutoPaginationFunction, + ): + test_auto_pagination( + http_client=self.http_client, + list_function=self.authorization.list_resources, + expected_all_page_data=mock_resources_multiple, + list_function_params={"organization_id": MOCK_ORG_ID}, + ) From a50f5d59024e37ba997d23545e7c8bc5b1321583 Mon Sep 17 00:00:00 2001 From: swaroopThereItIs Date: Tue, 24 Feb 2026 11:17:25 -1000 Subject: [PATCH 10/42] FGA_3: check() (#568) --- src/workos/authorization.py | 46 +++++++ src/workos/types/authorization/__init__.py | 5 + .../authorization/resource_identifier.py | 15 +++ tests/test_authorization_check.py | 127 ++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 src/workos/types/authorization/resource_identifier.py create mode 100644 tests/test_authorization_check.py diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 3d545054..d0572a69 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -4,12 +4,14 @@ from pydantic import TypeAdapter from typing_extensions import TypedDict +from workos.types.authorization.access_evaluation import AccessEvaluation from workos.types.authorization.environment_role import ( EnvironmentRole, EnvironmentRoleList, ) from workos.types.authorization.organization_role import OrganizationRole from workos.types.authorization.permission import Permission +from workos.types.authorization.resource_identifier import ResourceIdentifier from workos.types.authorization.resource import Resource from workos.types.authorization.role import Role, RoleList from workos.types.list_resource import ( @@ -268,6 +270,14 @@ def delete_resource_by_external_id( cascade_delete: Optional[bool] = None, ) -> SyncOrAsync[None]: ... + def check( + self, + organization_membership_id: str, + *, + permission_slug: str, + resource: ResourceIdentifier, + ) -> SyncOrAsync[AccessEvaluation]: ... + class Authorization(AuthorizationModule): _http_client: SyncHTTPClient @@ -722,6 +732,24 @@ def delete_resource_by_external_id( params=params if params else None, ) + def check( + self, + organization_membership_id: str, + *, + permission_slug: str, + resource: ResourceIdentifier, + ) -> AccessEvaluation: + json: Dict[str, Any] = {"permission_slug": permission_slug} + json.update(resource) + + response = self._http_client.request( + f"authorization/organization_memberships/{organization_membership_id}/check", + method=REQUEST_METHOD_POST, + json=json, + ) + + return AccessEvaluation.model_validate(response) + class AsyncAuthorization(AuthorizationModule): _http_client: AsyncHTTPClient @@ -1175,3 +1203,21 @@ async def delete_resource_by_external_id( method=REQUEST_METHOD_DELETE, params=params if params else None, ) + + async def check( + self, + organization_membership_id: str, + *, + permission_slug: str, + resource: ResourceIdentifier, + ) -> AccessEvaluation: + json: Dict[str, Any] = {"permission_slug": permission_slug} + json.update(resource) + + response = await self._http_client.request( + f"authorization/organization_memberships/{organization_membership_id}/check", + method=REQUEST_METHOD_POST, + json=json, + ) + + return AccessEvaluation.model_validate(response) diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py index 9eb705a0..93946662 100644 --- a/src/workos/types/authorization/__init__.py +++ b/src/workos/types/authorization/__init__.py @@ -13,6 +13,11 @@ ) from workos.types.authorization.permission import Permission from workos.types.authorization.resource import Resource +from workos.types.authorization.resource_identifier import ( + ResourceIdentifier, + ResourceIdentifierByExternalId, + ResourceIdentifierById, +) from workos.types.authorization.role import ( Role, RoleList, diff --git a/src/workos/types/authorization/resource_identifier.py b/src/workos/types/authorization/resource_identifier.py new file mode 100644 index 00000000..081a175d --- /dev/null +++ b/src/workos/types/authorization/resource_identifier.py @@ -0,0 +1,15 @@ +from typing import Union + +from typing_extensions import TypedDict + + +class ResourceIdentifierById(TypedDict): + resource_id: str + + +class ResourceIdentifierByExternalId(TypedDict): + resource_external_id: str + resource_type_slug: str + + +ResourceIdentifier = Union[ResourceIdentifierById, ResourceIdentifierByExternalId] diff --git a/tests/test_authorization_check.py b/tests/test_authorization_check.py new file mode 100644 index 00000000..09fad9ba --- /dev/null +++ b/tests/test_authorization_check.py @@ -0,0 +1,127 @@ +from typing import Union + +import pytest +from tests.utils.syncify import syncify +from workos.authorization import AsyncAuthorization, Authorization +from workos.types.authorization.resource_identifier import ( + ResourceIdentifierByExternalId, + ResourceIdentifierById, +) + + +@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) +class TestAuthorizationCheck: + @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_check_authorized(self): + return {"authorized": True} + + @pytest.fixture + def mock_check_unauthorized(self): + return {"authorized": False} + + def test_check_authorized( + self, mock_check_authorized, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_check_authorized, 200 + ) + + result = syncify( + self.authorization.check( + "om_01ABC", + permission_slug="documents:read", + resource=ResourceIdentifierById(resource_id="res_01ABC"), + ) + ) + + assert result.authorized is True + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01ABC/check" + ) + + def test_check_unauthorized( + self, mock_check_unauthorized, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_check_unauthorized, 200 + ) + + result = syncify( + self.authorization.check( + "om_01ABC", + permission_slug="documents:write", + resource=ResourceIdentifierById(resource_id="res_01ABC"), + ) + ) + + assert result.authorized is False + assert request_kwargs["method"] == "post" + + def test_check_with_resource_id( + self, mock_check_authorized, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_check_authorized, 200 + ) + + syncify( + self.authorization.check( + "om_01ABC", + permission_slug="documents:read", + resource=ResourceIdentifierById(resource_id="res_01XYZ"), + ) + ) + + assert request_kwargs["json"] == { + "permission_slug": "documents:read", + "resource_id": "res_01XYZ", + } + + def test_check_with_resource_external_id( + self, mock_check_authorized, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_check_authorized, 200 + ) + + syncify( + self.authorization.check( + "om_01ABC", + permission_slug="documents:read", + resource=ResourceIdentifierByExternalId( + resource_external_id="ext_doc_123", + resource_type_slug="document", + ), + ) + ) + + assert request_kwargs["json"] == { + "permission_slug": "documents:read", + "resource_external_id": "ext_doc_123", + "resource_type_slug": "document", + } + + def test_check_url_construction( + self, mock_check_authorized, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_check_authorized, 200 + ) + + syncify( + self.authorization.check( + "om_01MEMBERSHIP", + permission_slug="admin:access", + resource=ResourceIdentifierById(resource_id="res_01ABC"), + ) + ) + + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01MEMBERSHIP/check" + ) From 3eca552bc699a0892a638d973f19fe3bbaa244ea Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Tue, 24 Feb 2026 12:52:25 -1000 Subject: [PATCH 11/42] remove organization_name for list --- .../types/authorization/organization_membership.py | 9 --------- tests/test_authorization_types.py | 2 -- 2 files changed, 11 deletions(-) diff --git a/src/workos/types/authorization/organization_membership.py b/src/workos/types/authorization/organization_membership.py index a5390bb0..a709ba0c 100644 --- a/src/workos/types/authorization/organization_membership.py +++ b/src/workos/types/authorization/organization_membership.py @@ -8,19 +8,10 @@ class AuthorizationOrganizationMembership(WorkOSModel): - """Representation of an Organization Membership returned by Authorization endpoints. - - This is a separate type from the user_management OrganizationMembership because - authorization endpoints return memberships without the ``role`` field and include - ``organization_name``. Additionally, ``custom_attributes`` is optional here as - authorization endpoints may omit it. - """ - object: Literal["organization_membership"] id: str user_id: str organization_id: str - organization_name: str status: LiteralOrUntyped[OrganizationMembershipStatus] custom_attributes: Optional[Mapping[str, Any]] = None created_at: str diff --git a/tests/test_authorization_types.py b/tests/test_authorization_types.py index a3480bb5..040cc20a 100644 --- a/tests/test_authorization_types.py +++ b/tests/test_authorization_types.py @@ -112,7 +112,6 @@ def test_membership_deserialization(self): "id": "om_01ABC", "user_id": "user_01ABC", "organization_id": "org_01ABC", - "organization_name": "Test Org", "status": "active", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", @@ -123,7 +122,6 @@ def test_membership_deserialization(self): assert membership.id == "om_01ABC" assert membership.user_id == "user_01ABC" assert membership.organization_id == "org_01ABC" - assert membership.organization_name == "Test Org" assert membership.status == "active" assert membership.custom_attributes is None From fa170f098735de76a624e603ed60a24bae173333 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Tue, 24 Feb 2026 12:59:52 -1000 Subject: [PATCH 12/42] casade --- .claude/settings.local.json | 9 +++ src/workos/authorization.py | 62 ++++++++++--------- tests/test_authorization_resource_crud.py | 2 +- ...test_authorization_resource_external_id.py | 2 +- 4 files changed, 43 insertions(+), 32 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..25464e8c --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(gh api:*)", + "Bash(gh pr diff:*)", + "Bash(uv run pytest:*)" + ] + } +} diff --git a/src/workos/authorization.py b/src/workos/authorization.py index d0572a69..5deeb143 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -621,16 +621,16 @@ def delete_resource( *, cascade_delete: Optional[bool] = None, ) -> None: - if cascade_delete is not None: - self._http_client.delete_with_body( - f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", - json={"cascade_delete": cascade_delete}, - ) - else: - self._http_client.request( - f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", - method=REQUEST_METHOD_DELETE, - ) + params = ( + {"cascade_delete": str(cascade_delete).lower()} + if cascade_delete is not None + else None + ) + self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_DELETE, + params=params, + ) def list_resources( self, @@ -722,14 +722,15 @@ def delete_resource_by_external_id( cascade_delete: Optional[bool] = None, ) -> None: path = f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}" - params: Dict[str, bool] = {} - if cascade_delete is not None: - params["cascade_delete"] = cascade_delete - + params = ( + {"cascade_delete": str(cascade_delete).lower()} + if cascade_delete is not None + else None + ) self._http_client.request( path, method=REQUEST_METHOD_DELETE, - params=params if params else None, + params=params, ) def check( @@ -1093,16 +1094,16 @@ async def delete_resource( *, cascade_delete: Optional[bool] = None, ) -> None: - if cascade_delete is not None: - await self._http_client.delete_with_body( - f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", - json={"cascade_delete": cascade_delete}, - ) - else: - await self._http_client.request( - f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", - method=REQUEST_METHOD_DELETE, - ) + params = ( + {"cascade_delete": str(cascade_delete).lower()} + if cascade_delete is not None + else None + ) + await self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_DELETE, + params=params, + ) async def list_resources( self, @@ -1194,14 +1195,15 @@ async def delete_resource_by_external_id( cascade_delete: Optional[bool] = None, ) -> None: path = f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}" - params: Dict[str, bool] = {} - if cascade_delete is not None: - params["cascade_delete"] = cascade_delete - + params = ( + {"cascade_delete": str(cascade_delete).lower()} + if cascade_delete is not None + else None + ) await self._http_client.request( path, method=REQUEST_METHOD_DELETE, - params=params if params else None, + params=params, ) async def check( diff --git a/tests/test_authorization_resource_crud.py b/tests/test_authorization_resource_crud.py index 3ff79a2d..ab9f4798 100644 --- a/tests/test_authorization_resource_crud.py +++ b/tests/test_authorization_resource_crud.py @@ -254,4 +254,4 @@ def test_delete_resource_with_cascade(self, capture_and_mock_http_client_request assert response is None assert request_kwargs["method"] == "delete" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["json"] == {"cascade_delete": True} + assert request_kwargs["params"] == {"cascade_delete": "true"} diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py index 10eb6b82..a43f1e4a 100644 --- a/tests/test_authorization_resource_external_id.py +++ b/tests/test_authorization_resource_external_id.py @@ -186,7 +186,7 @@ def test_delete_resource_by_external_id_with_cascade( assert request_kwargs["url"].endswith( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) - assert request_kwargs["params"] == {"cascade_delete": True} + assert request_kwargs["params"] == {"cascade_delete": "true"} # --- list_resources --- From 035f1c33bbe24be71c502340bc4911e61faed334 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Tue, 24 Feb 2026 13:05:13 -1000 Subject: [PATCH 13/42] lol --- src/workos/authorization.py | 12 ++++--- ...test_authorization_resource_external_id.py | 33 +++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 5deeb143..12898590 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -258,7 +258,7 @@ def update_resource_by_external_id( external_id: str, *, name: Optional[str] = None, - description: Optional[str] = None, + description: Union[str, None, _Unset] = UNSET, ) -> SyncOrAsync[Resource]: ... def delete_resource_by_external_id( @@ -697,18 +697,19 @@ def update_resource_by_external_id( external_id: str, *, name: Optional[str] = None, - description: Optional[str] = None, + description: Union[str, None, _Unset] = UNSET, ) -> Resource: json: Dict[str, Any] = {} if name is not None: json["name"] = name - if description is not None: + if not isinstance(description, _Unset): json["description"] = description response = self._http_client.request( f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}", method=REQUEST_METHOD_PATCH, json=json, + exclude_none=False, ) return Resource.model_validate(response) @@ -1170,18 +1171,19 @@ async def update_resource_by_external_id( external_id: str, *, name: Optional[str] = None, - description: Optional[str] = None, + description: Union[str, None, _Unset] = UNSET, ) -> Resource: json: Dict[str, Any] = {} if name is not None: json["name"] = name - if description is not None: + if not isinstance(description, _Unset): json["description"] = description response = await self._http_client.request( f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}", method=REQUEST_METHOD_PATCH, json=json, + exclude_none=False, ) return Resource.model_validate(response) diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py index a43f1e4a..b5c189c9 100644 --- a/tests/test_authorization_resource_external_id.py +++ b/tests/test_authorization_resource_external_id.py @@ -140,6 +140,39 @@ def test_update_resource_by_external_id_empty( assert request_kwargs["method"] == "patch" assert request_kwargs["json"] == {} + def test_update_resource_by_external_id_clear_description( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + syncify( + self.authorization.update_resource_by_external_id( + MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID, description=None + ) + ) + + assert request_kwargs["method"] == "patch" + assert request_kwargs["json"] == {"description": None} + + def test_update_resource_by_external_id_without_description( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + resource = syncify( + self.authorization.update_resource_by_external_id( + MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID, name="Updated Name" + ) + ) + + assert resource.id == "res_01ABC" + assert request_kwargs["method"] == "patch" + assert request_kwargs["json"] == {"name": "Updated Name"} + # --- delete_resource_by_external_id --- def test_delete_resource_by_external_id_without_cascade( From 8fbbc5f2721e6d9861c9f790afa55f2a9c52c537 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Wed, 25 Feb 2026 10:49:58 -1000 Subject: [PATCH 14/42] moar --- .claude/settings.local.json | 9 ------- src/workos/authorization.py | 12 ++++----- src/workos/types/authorization/__init__.py | 2 +- .../authorization/access_check_response.py | 6 +++++ .../types/authorization/access_evaluation.py | 7 ------ .../authorization/organization_membership.py | 1 - tests/test_authorization_types.py | 25 +++---------------- 7 files changed, 17 insertions(+), 45 deletions(-) delete mode 100644 .claude/settings.local.json create mode 100644 src/workos/types/authorization/access_check_response.py delete mode 100644 src/workos/types/authorization/access_evaluation.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 25464e8c..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(gh api:*)", - "Bash(gh pr diff:*)", - "Bash(uv run pytest:*)" - ] - } -} diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 12898590..731c8018 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -4,7 +4,7 @@ from pydantic import TypeAdapter from typing_extensions import TypedDict -from workos.types.authorization.access_evaluation import AccessEvaluation +from workos.types.authorization.access_check_response import AccessCheckResponse from workos.types.authorization.environment_role import ( EnvironmentRole, EnvironmentRoleList, @@ -276,7 +276,7 @@ def check( *, permission_slug: str, resource: ResourceIdentifier, - ) -> SyncOrAsync[AccessEvaluation]: ... + ) -> SyncOrAsync[AccessCheckResponse]: ... class Authorization(AuthorizationModule): @@ -740,7 +740,7 @@ def check( *, permission_slug: str, resource: ResourceIdentifier, - ) -> AccessEvaluation: + ) -> AccessCheckResponse: json: Dict[str, Any] = {"permission_slug": permission_slug} json.update(resource) @@ -750,7 +750,7 @@ def check( json=json, ) - return AccessEvaluation.model_validate(response) + return AccessCheckResponse.model_validate(response) class AsyncAuthorization(AuthorizationModule): @@ -1214,7 +1214,7 @@ async def check( *, permission_slug: str, resource: ResourceIdentifier, - ) -> AccessEvaluation: + ) -> AccessCheckResponse: json: Dict[str, Any] = {"permission_slug": permission_slug} json.update(resource) @@ -1224,4 +1224,4 @@ async def check( json=json, ) - return AccessEvaluation.model_validate(response) + return AccessCheckResponse.model_validate(response) diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py index 93946662..58d690d4 100644 --- a/src/workos/types/authorization/__init__.py +++ b/src/workos/types/authorization/__init__.py @@ -1,4 +1,4 @@ -from workos.types.authorization.access_evaluation import AccessEvaluation +from workos.types.authorization.access_evaluation import AccessCheckResponse from workos.types.authorization.environment_role import ( EnvironmentRole, EnvironmentRoleList, diff --git a/src/workos/types/authorization/access_check_response.py b/src/workos/types/authorization/access_check_response.py new file mode 100644 index 00000000..76e79317 --- /dev/null +++ b/src/workos/types/authorization/access_check_response.py @@ -0,0 +1,6 @@ +from workos.types.workos_model import WorkOSModel + + +class AccessCheckResponse(WorkOSModel): + + authorized: bool diff --git a/src/workos/types/authorization/access_evaluation.py b/src/workos/types/authorization/access_evaluation.py deleted file mode 100644 index 6b2a22af..00000000 --- a/src/workos/types/authorization/access_evaluation.py +++ /dev/null @@ -1,7 +0,0 @@ -from workos.types.workos_model import WorkOSModel - - -class AccessEvaluation(WorkOSModel): - """Representation of a WorkOS Authorization access check result.""" - - authorized: bool diff --git a/src/workos/types/authorization/organization_membership.py b/src/workos/types/authorization/organization_membership.py index a709ba0c..84466f4d 100644 --- a/src/workos/types/authorization/organization_membership.py +++ b/src/workos/types/authorization/organization_membership.py @@ -13,6 +13,5 @@ class AuthorizationOrganizationMembership(WorkOSModel): user_id: str organization_id: str status: LiteralOrUntyped[OrganizationMembershipStatus] - custom_attributes: Optional[Mapping[str, Any]] = None created_at: str updated_at: str diff --git a/tests/test_authorization_types.py b/tests/test_authorization_types.py index 040cc20a..603cd29f 100644 --- a/tests/test_authorization_types.py +++ b/tests/test_authorization_types.py @@ -2,7 +2,7 @@ AuthorizationOrganizationMembership.""" from workos.types.authorization import ( - AccessEvaluation, + AccessCheckResponse, AuthorizationOrganizationMembership, Resource, RoleAssignment, @@ -13,15 +13,15 @@ class TestAccessEvaluation: def test_authorized_true(self): - result = AccessEvaluation(authorized=True) + result = AccessCheckResponse(authorized=True) assert result.authorized is True def test_authorized_false(self): - result = AccessEvaluation(authorized=False) + result = AccessCheckResponse(authorized=False) assert result.authorized is False def test_from_dict(self): - result = AccessEvaluation.model_validate({"authorized": True}) + result = AccessCheckResponse.model_validate({"authorized": True}) assert result.authorized is True @@ -123,20 +123,3 @@ def test_membership_deserialization(self): assert membership.user_id == "user_01ABC" assert membership.organization_id == "org_01ABC" assert membership.status == "active" - assert membership.custom_attributes is None - - def test_membership_with_custom_attributes(self): - data = { - "object": "organization_membership", - "id": "om_01ABC", - "user_id": "user_01ABC", - "organization_id": "org_01ABC", - "organization_name": "Test Org", - "status": "active", - "custom_attributes": {"department": "Engineering"}, - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T00:00:00Z", - } - membership = AuthorizationOrganizationMembership.model_validate(data) - - assert membership.custom_attributes == {"department": "Engineering"} From 7342ad4e07cef2e2f421204872b1a90429368523 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Wed, 25 Feb 2026 12:43:30 -1000 Subject: [PATCH 15/42] Create --- src/workos/authorization.py | 18 +-- src/workos/types/authorization/__init__.py | 2 +- .../authorization/access_check_response.py | 1 - tests/test_authorization_resource_crud.py | 140 +++++++++++++----- 4 files changed, 115 insertions(+), 46 deletions(-) diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 731c8018..7fa6fbcd 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -206,12 +206,12 @@ def get_resource(self, resource_id: str) -> SyncOrAsync[Resource]: ... def create_resource( self, *, - resource_type_slug: str, - organization_id: str, external_id: str, name: str, - parent: Optional[ParentResource] = None, description: Optional[str] = None, + resource_type_slug: str, + organization_id: str, + parent: Optional[ParentResource] = None, ) -> SyncOrAsync[Resource]: ... def update_resource( @@ -567,12 +567,12 @@ def get_resource(self, resource_id: str) -> Resource: def create_resource( self, *, - resource_type_slug: str, - organization_id: str, external_id: str, name: str, - parent: Optional[ParentResource] = None, description: Optional[str] = None, + resource_type_slug: str, + organization_id: str, + parent: Optional[ParentResource] = None, ) -> Resource: json: Dict[str, Any] = { "resource_type_slug": resource_type_slug, @@ -1041,12 +1041,12 @@ async def get_resource(self, resource_id: str) -> Resource: async def create_resource( self, *, - resource_type_slug: str, - organization_id: str, external_id: str, name: str, - parent: Optional[ParentResource] = None, description: Optional[str] = None, + resource_type_slug: str, + organization_id: str, + parent: Optional[ParentResource] = None, ) -> Resource: json: Dict[str, Any] = { "resource_type_slug": resource_type_slug, diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py index 58d690d4..2ece4449 100644 --- a/src/workos/types/authorization/__init__.py +++ b/src/workos/types/authorization/__init__.py @@ -1,4 +1,4 @@ -from workos.types.authorization.access_evaluation import AccessCheckResponse +from workos.types.authorization.access_check_response import AccessCheckResponse from workos.types.authorization.environment_role import ( EnvironmentRole, EnvironmentRoleList, diff --git a/src/workos/types/authorization/access_check_response.py b/src/workos/types/authorization/access_check_response.py index 76e79317..2515b763 100644 --- a/src/workos/types/authorization/access_check_response.py +++ b/src/workos/types/authorization/access_check_response.py @@ -2,5 +2,4 @@ class AccessCheckResponse(WorkOSModel): - authorized: bool diff --git a/tests/test_authorization_resource_crud.py b/tests/test_authorization_resource_crud.py index ab9f4798..21652c04 100644 --- a/tests/test_authorization_resource_crud.py +++ b/tests/test_authorization_resource_crud.py @@ -32,8 +32,7 @@ def test_get_resource(self, mock_resource, capture_and_mock_http_client_request) assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") # --- create_resource --- - - def test_create_resource_required_fields_only( + def test_create_resource_with_parent_resource_id( self, mock_resource, capture_and_mock_http_client_request ): request_kwargs = capture_and_mock_http_client_request( @@ -42,26 +41,30 @@ def test_create_resource_required_fields_only( resource = syncify( self.authorization.create_resource( - resource_type_slug="document", organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + resource_type_slug="document", external_id="ext_123", - name="Test Resource", + name="Q4 Budget Report", + description="Financial report for Q4 2025", parent={"parent_resource_id": "res_01PARENT"}, ) ) - assert resource.id == "res_01ABC" - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["json"] == { - "resource_type_slug": "document", "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "resource_type_slug": "document", "external_id": "ext_123", - "name": "Test Resource", + "name": "Q4 Budget Report", + "description": "Financial report for Q4 2025", "parent_resource_id": "res_01PARENT", } + assert "parent_resource_external_id" not in request_kwargs["json"] + assert "parent_resource_type_slug" not in request_kwargs["json"] - def test_create_resource_without_parent( + assert resource.object == "authorization_resource" + assert resource.id == "res_01ABC" + + def test_create_resource_with_parent_resource_id_and_no_description( self, mock_resource, capture_and_mock_http_client_request ): request_kwargs = capture_and_mock_http_client_request( @@ -70,81 +73,137 @@ def test_create_resource_without_parent( resource = syncify( self.authorization.create_resource( - resource_type_slug="document", organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + resource_type_slug="document", external_id="ext_123", - name="Test Resource", + name="Q4 Budget Report", + parent={"parent_resource_id": "res_01PARENT"}, ) ) - assert resource.id == "res_01ABC" - assert request_kwargs["method"] == "post" assert request_kwargs["json"] == { - "resource_type_slug": "document", "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "resource_type_slug": "document", "external_id": "ext_123", - "name": "Test Resource", + "name": "Q4 Budget Report", + "parent_resource_id": "res_01PARENT", } + assert resource.object == "authorization_resource" + assert resource.id == "res_01ABC" - def test_create_resource_with_all_optional_fields( + def test_create_resource_with_parent_resource_id_and_none_description( self, mock_resource, capture_and_mock_http_client_request ): request_kwargs = capture_and_mock_http_client_request( self.http_client, mock_resource, 201 ) - syncify( + resource = syncify( self.authorization.create_resource( - resource_type_slug="document", organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + resource_type_slug="document", external_id="ext_123", - name="Test Resource", + name="Q4 Budget Report", + description=None, parent={"parent_resource_id": "res_01PARENT"}, - description="A test document", ) ) assert request_kwargs["json"] == { - "resource_type_slug": "document", "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "resource_type_slug": "document", "external_id": "ext_123", - "name": "Test Resource", + "name": "Q4 Budget Report", "parent_resource_id": "res_01PARENT", - "description": "A test document", } + assert "parent_resource_external_id" not in request_kwargs["json"] + assert "parent_resource_type_slug" not in request_kwargs["json"] + assert "description" not in request_kwargs["json"] + assert resource.object == "authorization_resource" + assert resource.id == "res_01ABC" - def test_create_resource_with_parent_by_id( + def test_create_resource_with_parent_external_id( self, mock_resource, capture_and_mock_http_client_request ): request_kwargs = capture_and_mock_http_client_request( self.http_client, mock_resource, 201 ) - syncify( + resource = syncify( self.authorization.create_resource( - resource_type_slug="document", organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + resource_type_slug="document", external_id="ext_123", - name="Test Resource", - parent={"parent_resource_id": "res_01PARENT"}, + name="Q4 Budget Report", + description="Financial report for Q4 2025", + parent={ + "parent_resource_external_id": "ext_parent_456", + "parent_resource_type_slug": "folder", + }, ) ) - assert request_kwargs["json"]["parent_resource_id"] == "res_01PARENT" + assert request_kwargs["json"] == { + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "resource_type_slug": "document", + "external_id": "ext_123", + "name": "Q4 Budget Report", + "description": "Financial report for Q4 2025", + "parent_resource_external_id": "ext_parent_456", + "parent_resource_type_slug": "folder", + } + assert "parent_resource_id" not in request_kwargs["json"] - def test_create_resource_with_parent_by_external_id( + assert resource.object == "authorization_resource" + assert resource.id == "res_01ABC" + + def test_create_resource_with_parent_external_id_and_no_description( self, mock_resource, capture_and_mock_http_client_request ): request_kwargs = capture_and_mock_http_client_request( self.http_client, mock_resource, 201 ) - syncify( + resource = syncify( self.authorization.create_resource( + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", resource_type_slug="document", + external_id="ext_123", + name="Q4 Budget Report", + parent={ + "parent_resource_external_id": "ext_parent_456", + "parent_resource_type_slug": "folder", + }, + ) + ) + + assert request_kwargs["json"] == { + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "resource_type_slug": "document", + "external_id": "ext_123", + "name": "Q4 Budget Report", + "parent_resource_external_id": "ext_parent_456", + "parent_resource_type_slug": "folder", + } + assert "parent_resource_id" not in request_kwargs["json"] + + assert resource.object == "authorization_resource" + assert resource.id == "res_01ABC" + + def test_create_resource_with_parent_external_id_and_none_description( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 201 + ) + + resource = syncify( + self.authorization.create_resource( organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + resource_type_slug="document", external_id="ext_123", - name="Test Resource", + name="Q4 Budget Report", + description=None, parent={ "parent_resource_external_id": "ext_parent_456", "parent_resource_type_slug": "folder", @@ -152,8 +211,19 @@ def test_create_resource_with_parent_by_external_id( ) ) - assert request_kwargs["json"]["parent_resource_external_id"] == "ext_parent_456" - assert request_kwargs["json"]["parent_resource_type_slug"] == "folder" + assert request_kwargs["json"] == { + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "resource_type_slug": "document", + "external_id": "ext_123", + "name": "Q4 Budget Report", + "parent_resource_external_id": "ext_parent_456", + "parent_resource_type_slug": "folder", + } + assert "parent_resource_id" not in request_kwargs["json"] + assert "description" not in request_kwargs["json"] + + assert resource.object == "authorization_resource" + assert resource.id == "res_01ABC" # --- update_resource --- From 3f2516165c887d5a5e24651bafcfe8e8c5920bb1 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Wed, 25 Feb 2026 13:10:41 -1000 Subject: [PATCH 16/42] test update --- tests/test_authorization_resource_crud.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_authorization_resource_crud.py b/tests/test_authorization_resource_crud.py index 21652c04..d0adb082 100644 --- a/tests/test_authorization_resource_crud.py +++ b/tests/test_authorization_resource_crud.py @@ -50,6 +50,8 @@ def test_create_resource_with_parent_resource_id( ) ) + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["json"] == { "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "resource_type_slug": "document", @@ -81,6 +83,8 @@ def test_create_resource_with_parent_resource_id_and_no_description( ) ) + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["json"] == { "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "resource_type_slug": "document", @@ -109,6 +113,8 @@ def test_create_resource_with_parent_resource_id_and_none_description( ) ) + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["json"] == { "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "resource_type_slug": "document", @@ -143,6 +149,8 @@ def test_create_resource_with_parent_external_id( ) ) + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["json"] == { "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "resource_type_slug": "document", @@ -177,6 +185,8 @@ def test_create_resource_with_parent_external_id_and_no_description( ) ) + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["json"] == { "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "resource_type_slug": "document", @@ -211,6 +221,8 @@ def test_create_resource_with_parent_external_id_and_none_description( ) ) + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["json"] == { "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "resource_type_slug": "document", From dfca2918e1e5a25a2f846616afc618c7c55de78d Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Wed, 25 Feb 2026 13:55:18 -1000 Subject: [PATCH 17/42] break outp --- .claude/settings.local.json | 12 ++++++++++ .../authorization/organization_membership.py | 18 +++------------ .../organization_membership.py | 22 ++++++++++--------- 3 files changed, 27 insertions(+), 25 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..6fa5c618 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(uv run pytest:*)", + "Bash(python3:*)", + "Bash(uv run python:*)", + "Bash(gh pr list:*)", + "Bash(gh api:*)", + "Bash(uv run mypy:*)" + ] + } +} diff --git a/src/workos/types/authorization/organization_membership.py b/src/workos/types/authorization/organization_membership.py index 84466f4d..f24e9a27 100644 --- a/src/workos/types/authorization/organization_membership.py +++ b/src/workos/types/authorization/organization_membership.py @@ -1,17 +1,5 @@ -from typing import Any, Literal, Mapping, Optional - -from workos.types.user_management.organization_membership_status import ( - OrganizationMembershipStatus, +from workos.types.user_management.organization_membership import ( + BaseOrganizationMembership, ) -from workos.types.workos_model import WorkOSModel -from workos.typing.literals import LiteralOrUntyped - -class AuthorizationOrganizationMembership(WorkOSModel): - object: Literal["organization_membership"] - id: str - user_id: str - organization_id: str - status: LiteralOrUntyped[OrganizationMembershipStatus] - created_at: str - updated_at: str +AuthorizationOrganizationMembership = BaseOrganizationMembership diff --git a/src/workos/types/user_management/organization_membership.py b/src/workos/types/user_management/organization_membership.py index df9e5acf..0a60afea 100644 --- a/src/workos/types/user_management/organization_membership.py +++ b/src/workos/types/user_management/organization_membership.py @@ -8,20 +8,22 @@ from workos.typing.literals import LiteralOrUntyped -class OrganizationMembershipRole(TypedDict): - slug: str - - -class OrganizationMembership(WorkOSModel): - """Representation of an WorkOS Organization Membership.""" - +class BaseOrganizationMembership(WorkOSModel): object: Literal["organization_membership"] id: str user_id: str organization_id: str - role: OrganizationMembershipRole - roles: Optional[Sequence[OrganizationMembershipRole]] = None status: LiteralOrUntyped[OrganizationMembershipStatus] - custom_attributes: Mapping[str, Any] + custom_attributes: Optional[Mapping[str, Any]] = None created_at: str updated_at: str + + +class OrganizationMembershipRole(TypedDict): + slug: str + + +class OrganizationMembership(BaseOrganizationMembership): + role: OrganizationMembershipRole + roles: Optional[Sequence[OrganizationMembershipRole]] = None + custom_attributes: Mapping[str, Any] From 5f72f0d18cc808ab1e4130e04f47308196ff606c Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Wed, 25 Feb 2026 14:06:41 -1000 Subject: [PATCH 18/42] lol --- tests/test_authorization_check.py | 3 + tests/test_authorization_resource_crud.py | 58 +++++++++++- ...test_authorization_resource_external_id.py | 90 +++++++++++++++++++ tests/utils/fixtures/mock_role_assignment.py | 31 ------- 4 files changed, 150 insertions(+), 32 deletions(-) delete mode 100644 tests/utils/fixtures/mock_role_assignment.py diff --git a/tests/test_authorization_check.py b/tests/test_authorization_check.py index 09fad9ba..9fa1849d 100644 --- a/tests/test_authorization_check.py +++ b/tests/test_authorization_check.py @@ -82,6 +82,8 @@ def test_check_with_resource_id( "permission_slug": "documents:read", "resource_id": "res_01XYZ", } + assert "resource_external_id" not in request_kwargs["json"] + assert "resource_type_slug" not in request_kwargs["json"] def test_check_with_resource_external_id( self, mock_check_authorized, capture_and_mock_http_client_request @@ -106,6 +108,7 @@ def test_check_with_resource_external_id( "resource_external_id": "ext_doc_123", "resource_type_slug": "document", } + assert "resource_id" not in request_kwargs["json"] def test_check_url_construction( self, mock_check_authorized, capture_and_mock_http_client_request diff --git a/tests/test_authorization_resource_crud.py b/tests/test_authorization_resource_crud.py index d0adb082..9d928e4d 100644 --- a/tests/test_authorization_resource_crud.py +++ b/tests/test_authorization_resource_crud.py @@ -28,10 +28,47 @@ def test_get_resource(self, mock_resource, capture_and_mock_http_client_request) assert resource.id == "res_01ABC" assert resource.object == "authorization_resource" + assert resource.external_id == "ext_123" + assert resource.name == "Test Resource" + assert resource.resource_type_slug == "document" + assert resource.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" assert request_kwargs["method"] == "get" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") # --- create_resource --- + def test_create_resource_without_parent( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 201 + ) + + resource = syncify( + self.authorization.create_resource( + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + resource_type_slug="document", + external_id="ext_123", + name="Q4 Budget Report", + description="Financial report for Q4 2025", + ) + ) + + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith("/authorization/resources") + assert request_kwargs["json"] == { + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "resource_type_slug": "document", + "external_id": "ext_123", + "name": "Q4 Budget Report", + "description": "Financial report for Q4 2025", + } + assert "parent_resource_id" not in request_kwargs["json"] + assert "parent_resource_external_id" not in request_kwargs["json"] + assert "parent_resource_type_slug" not in request_kwargs["json"] + + assert resource.object == "authorization_resource" + assert resource.id == "res_01ABC" + def test_create_resource_with_parent_resource_id( self, mock_resource, capture_and_mock_http_client_request ): @@ -239,7 +276,7 @@ def test_create_resource_with_parent_external_id_and_none_description( # --- update_resource --- - def test_update_resource_with_meta( + def test_update_resource_with_name_and_description( self, mock_resource, capture_and_mock_http_client_request ): request_kwargs = capture_and_mock_http_client_request( @@ -321,6 +358,7 @@ def test_delete_resource_without_cascade( assert response is None assert request_kwargs["method"] == "delete" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + assert request_kwargs.get("params") is None def test_delete_resource_with_cascade(self, capture_and_mock_http_client_request): request_kwargs = capture_and_mock_http_client_request( @@ -337,3 +375,21 @@ def test_delete_resource_with_cascade(self, capture_and_mock_http_client_request assert request_kwargs["method"] == "delete" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") assert request_kwargs["params"] == {"cascade_delete": "true"} + + def test_delete_resource_with_cascade_false( + self, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, + status_code=202, + headers={"content-type": "text/plain; charset=utf-8"}, + ) + + response = syncify( + self.authorization.delete_resource("res_01ABC", cascade_delete=False) + ) + + assert response is None + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + assert request_kwargs["params"] == {"cascade_delete": "false"} diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py index b5c189c9..636d1661 100644 --- a/tests/test_authorization_resource_external_id.py +++ b/tests/test_authorization_resource_external_id.py @@ -195,6 +195,7 @@ def test_delete_resource_by_external_id_without_cascade( assert request_kwargs["url"].endswith( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) + assert request_kwargs.get("params") is None def test_delete_resource_by_external_id_with_cascade( self, capture_and_mock_http_client_request @@ -221,6 +222,31 @@ def test_delete_resource_by_external_id_with_cascade( ) assert request_kwargs["params"] == {"cascade_delete": "true"} + def test_delete_resource_by_external_id_with_cascade_false( + self, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, + status_code=202, + headers={"content-type": "text/plain; charset=utf-8"}, + ) + + response = syncify( + self.authorization.delete_resource_by_external_id( + MOCK_ORG_ID, + MOCK_RESOURCE_TYPE, + MOCK_EXTERNAL_ID, + cascade_delete=False, + ) + ) + + assert response is None + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith( + f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" + ) + assert request_kwargs["params"] == {"cascade_delete": "false"} + # --- list_resources --- def test_list_resources_with_results( @@ -270,6 +296,70 @@ def test_list_resources_with_resource_type_slug_filter( assert request_kwargs["method"] == "get" assert request_kwargs["params"]["resource_type_slug"] == "document" + def test_list_resources_with_parent_resource_id_filter( + self, mock_resources_list, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources( + organization_id=MOCK_ORG_ID, parent_resource_id="res_01PARENT" + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["params"]["parent_resource_id"] == "res_01PARENT" + + def test_list_resources_with_parent_resource_type_slug_filter( + self, mock_resources_list, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources( + organization_id=MOCK_ORG_ID, parent_resource_type_slug="folder" + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" + + def test_list_resources_with_parent_external_id_filter( + self, mock_resources_list, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources( + organization_id=MOCK_ORG_ID, parent_external_id="ext_parent_456" + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["params"]["parent_external_id"] == "ext_parent_456" + + def test_list_resources_with_search_filter( + self, mock_resources_list, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list, 200 + ) + + syncify( + self.authorization.list_resources( + organization_id=MOCK_ORG_ID, search="budget" + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["params"]["search"] == "budget" + def test_list_resources_with_pagination_params( self, mock_resources_list, capture_and_mock_http_client_request ): diff --git a/tests/utils/fixtures/mock_role_assignment.py b/tests/utils/fixtures/mock_role_assignment.py deleted file mode 100644 index 23b2dcde..00000000 --- a/tests/utils/fixtures/mock_role_assignment.py +++ /dev/null @@ -1,31 +0,0 @@ -import datetime - -from workos.types.authorization.role_assignment import ( - RoleAssignment, - RoleAssignmentResource, - RoleAssignmentRole, -) - - -class MockRoleAssignment(RoleAssignment): - def __init__( - self, - id: str = "ra_01ABC", - role_slug: str = "admin", - resource_id: str = "res_01ABC", - resource_external_id: str = "ext_123", - resource_type_slug: str = "document", - ): - now = datetime.datetime.now().isoformat() - super().__init__( - object="role_assignment", - id=id, - role=RoleAssignmentRole(slug=role_slug), - resource=RoleAssignmentResource( - id=resource_id, - external_id=resource_external_id, - resource_type_slug=resource_type_slug, - ), - created_at=now, - updated_at=now, - ) From a7c6d28944c64b87333e43517e238da3a72a02d1 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Wed, 25 Feb 2026 14:10:17 -1000 Subject: [PATCH 19/42] moar --- .claude/settings.local.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 6fa5c618..489c3d39 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,12 +1,7 @@ { "permissions": { "allow": [ - "Bash(uv run pytest:*)", - "Bash(python3:*)", - "Bash(uv run python:*)", - "Bash(gh pr list:*)", - "Bash(gh api:*)", - "Bash(uv run mypy:*)" + "Bash(uv run pytest:*)" ] } } From 0111cde9e917092ffe6572fa71dda99e434023d2 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Wed, 25 Feb 2026 14:10:39 -1000 Subject: [PATCH 20/42] moar --- .claude/settings.local.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 489c3d39..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(uv run pytest:*)" - ] - } -} From 6b8ca59cb360859a54076c6ecfb5b7d867e92cfc Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 03:52:52 -1000 Subject: [PATCH 21/42] refactor authorization resource --- src/workos/authorization.py | 71 ++++++++++--------- src/workos/types/authorization/__init__.py | 2 +- ...{resource.py => authorization_resource.py} | 2 +- src/workos/types/list_resource.py | 4 +- .../organization_membership.py | 3 +- ...crud.py => test_authorization_resource.py} | 0 tests/test_authorization_types.py | 8 +-- tests/utils/fixtures/mock_resource.py | 4 +- 8 files changed, 50 insertions(+), 44 deletions(-) rename src/workos/types/authorization/{resource.py => authorization_resource.py} (90%) rename tests/{test_authorization_resource_crud.py => test_authorization_resource.py} (100%) diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 7fa6fbcd..b518127f 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -12,7 +12,7 @@ from workos.types.authorization.organization_role import OrganizationRole from workos.types.authorization.permission import Permission from workos.types.authorization.resource_identifier import ResourceIdentifier -from workos.types.authorization.resource import Resource +from workos.types.authorization.authorization_resource import AuthorizationResource from workos.types.authorization.role import Role, RoleList from workos.types.list_resource import ( ListArgs, @@ -53,7 +53,10 @@ class ResourceListFilters(ListArgs, total=False): search: Optional[str] -ResourcesListResource = WorkOSListResource[Resource, ResourceListFilters, ListMetadata] +# TODO RENAME +ResourcesListResource = WorkOSListResource[ + AuthorizationResource, ResourceListFilters, ListMetadata +] class ParentResourceById(TypedDict): @@ -201,7 +204,7 @@ def add_environment_role_permission( # Resources - def get_resource(self, resource_id: str) -> SyncOrAsync[Resource]: ... + def get_resource(self, resource_id: str) -> SyncOrAsync[AuthorizationResource]: ... def create_resource( self, @@ -212,7 +215,7 @@ def create_resource( resource_type_slug: str, organization_id: str, parent: Optional[ParentResource] = None, - ) -> SyncOrAsync[Resource]: ... + ) -> SyncOrAsync[AuthorizationResource]: ... def update_resource( self, @@ -220,7 +223,7 @@ def update_resource( *, name: Optional[str] = None, description: Union[str, None, _Unset] = UNSET, - ) -> SyncOrAsync[Resource]: ... + ) -> SyncOrAsync[AuthorizationResource]: ... def delete_resource( self, @@ -249,7 +252,7 @@ def get_resource_by_external_id( organization_id: str, resource_type: str, external_id: str, - ) -> SyncOrAsync[Resource]: ... + ) -> SyncOrAsync[AuthorizationResource]: ... def update_resource_by_external_id( self, @@ -259,7 +262,7 @@ def update_resource_by_external_id( *, name: Optional[str] = None, description: Union[str, None, _Unset] = UNSET, - ) -> SyncOrAsync[Resource]: ... + ) -> SyncOrAsync[AuthorizationResource]: ... def delete_resource_by_external_id( self, @@ -554,15 +557,13 @@ def add_environment_role_permission( return EnvironmentRole.model_validate(response) - # Resources - - def get_resource(self, resource_id: str) -> Resource: + def get_resource(self, resource_id: str) -> AuthorizationResource: response = self._http_client.request( f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", method=REQUEST_METHOD_GET, ) - return Resource.model_validate(response) + return AuthorizationResource.model_validate(response) def create_resource( self, @@ -573,7 +574,7 @@ def create_resource( resource_type_slug: str, organization_id: str, parent: Optional[ParentResource] = None, - ) -> Resource: + ) -> AuthorizationResource: json: Dict[str, Any] = { "resource_type_slug": resource_type_slug, "organization_id": organization_id, @@ -591,7 +592,7 @@ def create_resource( json=json, ) - return Resource.model_validate(response) + return AuthorizationResource.model_validate(response) def update_resource( self, @@ -599,7 +600,7 @@ def update_resource( *, name: Optional[str] = None, description: Union[str, None, _Unset] = UNSET, - ) -> Resource: + ) -> AuthorizationResource: json: Dict[str, Any] = {} if name is not None: json["name"] = name @@ -613,7 +614,7 @@ def update_resource( exclude_none=False, ) - return Resource.model_validate(response) + return AuthorizationResource.model_validate(response) def delete_resource( self, @@ -671,10 +672,12 @@ def list_resources( params=list_params, ) - return WorkOSListResource[Resource, ResourceListFilters, ListMetadata]( + return WorkOSListResource[ + AuthorizationResource, ResourceListFilters, ListMetadata + ]( list_method=self.list_resources, list_args=list_params, - **ListPage[Resource](**response).model_dump(), + **ListPage[AuthorizationResource](**response).model_dump(), ) def get_resource_by_external_id( @@ -682,13 +685,13 @@ def get_resource_by_external_id( organization_id: str, resource_type: str, external_id: str, - ) -> Resource: + ) -> AuthorizationResource: response = self._http_client.request( f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}", method=REQUEST_METHOD_GET, ) - return Resource.model_validate(response) + return AuthorizationResource.model_validate(response) def update_resource_by_external_id( self, @@ -698,7 +701,7 @@ def update_resource_by_external_id( *, name: Optional[str] = None, description: Union[str, None, _Unset] = UNSET, - ) -> Resource: + ) -> AuthorizationResource: json: Dict[str, Any] = {} if name is not None: json["name"] = name @@ -712,7 +715,7 @@ def update_resource_by_external_id( exclude_none=False, ) - return Resource.model_validate(response) + return AuthorizationResource.model_validate(response) def delete_resource_by_external_id( self, @@ -1030,13 +1033,13 @@ async def add_environment_role_permission( # Resources - async def get_resource(self, resource_id: str) -> Resource: + async def get_resource(self, resource_id: str) -> AuthorizationResource: response = await self._http_client.request( f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", method=REQUEST_METHOD_GET, ) - return Resource.model_validate(response) + return AuthorizationResource.model_validate(response) async def create_resource( self, @@ -1047,7 +1050,7 @@ async def create_resource( resource_type_slug: str, organization_id: str, parent: Optional[ParentResource] = None, - ) -> Resource: + ) -> AuthorizationResource: json: Dict[str, Any] = { "resource_type_slug": resource_type_slug, "organization_id": organization_id, @@ -1065,7 +1068,7 @@ async def create_resource( json=json, ) - return Resource.model_validate(response) + return AuthorizationResource.model_validate(response) async def update_resource( self, @@ -1073,7 +1076,7 @@ async def update_resource( *, name: Optional[str] = None, description: Union[str, None, _Unset] = UNSET, - ) -> Resource: + ) -> AuthorizationResource: json: Dict[str, Any] = {} if name is not None: json["name"] = name @@ -1087,7 +1090,7 @@ async def update_resource( exclude_none=False, ) - return Resource.model_validate(response) + return AuthorizationResource.model_validate(response) async def delete_resource( self, @@ -1145,10 +1148,12 @@ async def list_resources( params=list_params, ) - return WorkOSListResource[Resource, ResourceListFilters, ListMetadata]( + return WorkOSListResource[ + AuthorizationResource, ResourceListFilters, ListMetadata + ]( list_method=self.list_resources, list_args=list_params, - **ListPage[Resource](**response).model_dump(), + **ListPage[AuthorizationResource](**response).model_dump(), ) async def get_resource_by_external_id( @@ -1156,13 +1161,13 @@ async def get_resource_by_external_id( organization_id: str, resource_type: str, external_id: str, - ) -> Resource: + ) -> AuthorizationResource: response = await self._http_client.request( f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}", method=REQUEST_METHOD_GET, ) - return Resource.model_validate(response) + return AuthorizationResource.model_validate(response) async def update_resource_by_external_id( self, @@ -1172,7 +1177,7 @@ async def update_resource_by_external_id( *, name: Optional[str] = None, description: Union[str, None, _Unset] = UNSET, - ) -> Resource: + ) -> AuthorizationResource: json: Dict[str, Any] = {} if name is not None: json["name"] = name @@ -1186,7 +1191,7 @@ async def update_resource_by_external_id( exclude_none=False, ) - return Resource.model_validate(response) + return AuthorizationResource.model_validate(response) async def delete_resource_by_external_id( self, diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py index 2ece4449..9b5dcdab 100644 --- a/src/workos/types/authorization/__init__.py +++ b/src/workos/types/authorization/__init__.py @@ -12,7 +12,7 @@ OrganizationRoleList, ) from workos.types.authorization.permission import Permission -from workos.types.authorization.resource import Resource +from workos.types.authorization.authorization_resource import AuthorizationResource from workos.types.authorization.resource_identifier import ( ResourceIdentifier, ResourceIdentifierByExternalId, diff --git a/src/workos/types/authorization/resource.py b/src/workos/types/authorization/authorization_resource.py similarity index 90% rename from src/workos/types/authorization/resource.py rename to src/workos/types/authorization/authorization_resource.py index 917673c4..ea722f9b 100644 --- a/src/workos/types/authorization/resource.py +++ b/src/workos/types/authorization/authorization_resource.py @@ -3,7 +3,7 @@ from workos.types.workos_model import WorkOSModel -class Resource(WorkOSModel): +class AuthorizationResource(WorkOSModel): """Representation of an Authorization Resource.""" object: Literal["authorization_resource"] diff --git a/src/workos/types/list_resource.py b/src/workos/types/list_resource.py index db07dcf0..5cbeed13 100644 --- a/src/workos/types/list_resource.py +++ b/src/workos/types/list_resource.py @@ -23,7 +23,7 @@ AuthorizationOrganizationMembership, ) from workos.types.authorization.permission import Permission -from workos.types.authorization.resource import Resource +from workos.types.authorization.authorization_resource import AuthorizationResource from workos.types.authorization.role_assignment import RoleAssignment from workos.types.directory_sync import ( Directory, @@ -64,7 +64,7 @@ Organization, OrganizationMembership, Permission, - Resource, + AuthorizationResource, RoleAssignment, AuthorizationOrganizationMembership, AuthorizationResource, diff --git a/src/workos/types/user_management/organization_membership.py b/src/workos/types/user_management/organization_membership.py index 190b8c95..a309d562 100644 --- a/src/workos/types/user_management/organization_membership.py +++ b/src/workos/types/user_management/organization_membership.py @@ -1,5 +1,6 @@ from typing import Any, Literal, Mapping, Optional, Sequence +from pydantic import Field from typing_extensions import TypedDict from workos.types.user_management.organization_membership_status import ( @@ -27,4 +28,4 @@ class OrganizationMembershipRole(TypedDict): class OrganizationMembership(BaseOrganizationMembership): role: OrganizationMembershipRole roles: Optional[Sequence[OrganizationMembershipRole]] = None - custom_attributes: Mapping[str, Any] + custom_attributes: Mapping[str, Any] = Field(default_factory=dict) diff --git a/tests/test_authorization_resource_crud.py b/tests/test_authorization_resource.py similarity index 100% rename from tests/test_authorization_resource_crud.py rename to tests/test_authorization_resource.py diff --git a/tests/test_authorization_types.py b/tests/test_authorization_types.py index 603cd29f..32ce0c3a 100644 --- a/tests/test_authorization_types.py +++ b/tests/test_authorization_types.py @@ -1,10 +1,10 @@ -"""Tests for new authorization types: Resource, RoleAssignment, AccessEvaluation, +"""Tests for new authorization types: AuthorizationResource, RoleAssignment, AccessEvaluation, AuthorizationOrganizationMembership.""" from workos.types.authorization import ( AccessCheckResponse, AuthorizationOrganizationMembership, - Resource, + AuthorizationResource, RoleAssignment, RoleAssignmentResource, RoleAssignmentRole, @@ -37,7 +37,7 @@ def test_resource_deserialization(self): "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", } - resource = Resource.model_validate(data) + resource = AuthorizationResource.model_validate(data) assert resource.object == "authorization_resource" assert resource.id == "res_01ABC" @@ -61,7 +61,7 @@ def test_resource_with_optional_fields(self): "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", } - resource = Resource.model_validate(data) + resource = AuthorizationResource.model_validate(data) assert resource.description == "A test document resource" assert resource.parent_resource_id == "res_01PARENT" diff --git a/tests/utils/fixtures/mock_resource.py b/tests/utils/fixtures/mock_resource.py index 825bf5fb..ddd38957 100644 --- a/tests/utils/fixtures/mock_resource.py +++ b/tests/utils/fixtures/mock_resource.py @@ -1,9 +1,9 @@ import datetime -from workos.types.authorization.resource import Resource +from workos.types.authorization.authorization_resource import AuthorizationResource -class MockResource(Resource): +class MockResource(AuthorizationResource): def __init__( self, id: str = "res_01ABC", From f04c33ee3887b230b72fbbd710931e683f17850f Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 04:04:05 -1000 Subject: [PATCH 22/42] Test updates --- tests/test_async_http_client.py | 8 ++ tests/test_authorization_resource.py | 79 ++++++++++--------- ...test_authorization_resource_external_id.py | 65 ++++++++++----- tests/test_authorization_types.py | 62 +++++++-------- tests/utils/fixtures/mock_resource.py | 13 +-- 5 files changed, 137 insertions(+), 90 deletions(-) diff --git a/tests/test_async_http_client.py b/tests/test_async_http_client.py index c9da72ba..04e29d00 100644 --- a/tests/test_async_http_client.py +++ b/tests/test_async_http_client.py @@ -273,6 +273,7 @@ async def test_request_includes_base_headers( await self.http_client.request("ok_place") + assert request_kwargs["url"].endswith("ok_place") default_headers = set( (header[0].lower(), header[1]) for header in self.http_client.default_headers.items() @@ -313,6 +314,8 @@ async def test_request_removes_none_parameter_values( method="get", params={"organization_id": None, "test": "value"}, ) + + assert request_kwargs["url"].endswith("/test") assert request_kwargs["params"] == {"test": "value"} async def test_request_removes_none_json_values( @@ -325,6 +328,8 @@ async def test_request_removes_none_json_values( method="post", json={"organization_id": None, "test": "value"}, ) + + assert request_kwargs["url"].endswith("/test") assert request_kwargs["json"] == {"test": "value"} async def test_delete_with_body_sends_json( @@ -338,6 +343,7 @@ async def test_delete_with_body_sends_json( ) assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith("/test") assert request_kwargs["json"] == {"resource_id": "res_01ABC"} async def test_delete_with_body_sends_params( @@ -351,6 +357,8 @@ async def test_delete_with_body_sends_params( params={"org_id": "org_01ABC"}, ) + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith("/test") assert request_kwargs["params"] == {"org_id": "org_01ABC"} assert request_kwargs["json"] == {"resource_id": "res_01ABC"} diff --git a/tests/test_authorization_resource.py b/tests/test_authorization_resource.py index 9d928e4d..112ad094 100644 --- a/tests/test_authorization_resource.py +++ b/tests/test_authorization_resource.py @@ -24,17 +24,22 @@ def test_get_resource(self, mock_resource, capture_and_mock_http_client_request) self.http_client, mock_resource, 200 ) - resource = syncify(self.authorization.get_resource("res_01ABC")) - - assert resource.id == "res_01ABC" - assert resource.object == "authorization_resource" - assert resource.external_id == "ext_123" - assert resource.name == "Test Resource" - assert resource.resource_type_slug == "document" - assert resource.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" + response = syncify(self.authorization.get_resource("res_01ABC")) + assert request_kwargs["method"] == "get" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + assert response.object == "authorization_resource" + assert response.id == "res_01ABC" + assert response.external_id == "ext_123" + assert response.name == "Test Resource" + assert response.description == "A test resource for unit tests" + assert response.resource_type_slug == "document" + assert response.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" + assert response.parent_resource_id == "res_01XYZ" + assert response.created_at == "2024-01-15T12:00:00.000Z" + assert response.updated_at == "2024-01-15T12:00:00.000Z" + # --- create_resource --- def test_create_resource_without_parent( self, mock_resource, capture_and_mock_http_client_request @@ -43,7 +48,7 @@ def test_create_resource_without_parent( self.http_client, mock_resource, 201 ) - resource = syncify( + response = syncify( self.authorization.create_resource( organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", resource_type_slug="document", @@ -66,8 +71,8 @@ def test_create_resource_without_parent( assert "parent_resource_external_id" not in request_kwargs["json"] assert "parent_resource_type_slug" not in request_kwargs["json"] - assert resource.object == "authorization_resource" - assert resource.id == "res_01ABC" + assert response.object == "authorization_resource" + assert response.id == "res_01ABC" def test_create_resource_with_parent_resource_id( self, mock_resource, capture_and_mock_http_client_request @@ -76,7 +81,7 @@ def test_create_resource_with_parent_resource_id( self.http_client, mock_resource, 201 ) - resource = syncify( + response = syncify( self.authorization.create_resource( organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", resource_type_slug="document", @@ -100,8 +105,8 @@ def test_create_resource_with_parent_resource_id( assert "parent_resource_external_id" not in request_kwargs["json"] assert "parent_resource_type_slug" not in request_kwargs["json"] - assert resource.object == "authorization_resource" - assert resource.id == "res_01ABC" + assert response.object == "authorization_resource" + assert response.id == "res_01ABC" def test_create_resource_with_parent_resource_id_and_no_description( self, mock_resource, capture_and_mock_http_client_request @@ -110,7 +115,7 @@ def test_create_resource_with_parent_resource_id_and_no_description( self.http_client, mock_resource, 201 ) - resource = syncify( + response = syncify( self.authorization.create_resource( organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", resource_type_slug="document", @@ -129,8 +134,8 @@ def test_create_resource_with_parent_resource_id_and_no_description( "name": "Q4 Budget Report", "parent_resource_id": "res_01PARENT", } - assert resource.object == "authorization_resource" - assert resource.id == "res_01ABC" + assert response.object == "authorization_resource" + assert response.id == "res_01ABC" def test_create_resource_with_parent_resource_id_and_none_description( self, mock_resource, capture_and_mock_http_client_request @@ -139,7 +144,7 @@ def test_create_resource_with_parent_resource_id_and_none_description( self.http_client, mock_resource, 201 ) - resource = syncify( + response = syncify( self.authorization.create_resource( organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", resource_type_slug="document", @@ -162,8 +167,8 @@ def test_create_resource_with_parent_resource_id_and_none_description( assert "parent_resource_external_id" not in request_kwargs["json"] assert "parent_resource_type_slug" not in request_kwargs["json"] assert "description" not in request_kwargs["json"] - assert resource.object == "authorization_resource" - assert resource.id == "res_01ABC" + assert response.object == "authorization_resource" + assert response.id == "res_01ABC" def test_create_resource_with_parent_external_id( self, mock_resource, capture_and_mock_http_client_request @@ -172,7 +177,7 @@ def test_create_resource_with_parent_external_id( self.http_client, mock_resource, 201 ) - resource = syncify( + response = syncify( self.authorization.create_resource( organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", resource_type_slug="document", @@ -199,8 +204,8 @@ def test_create_resource_with_parent_external_id( } assert "parent_resource_id" not in request_kwargs["json"] - assert resource.object == "authorization_resource" - assert resource.id == "res_01ABC" + assert response.object == "authorization_resource" + assert response.id == "res_01ABC" def test_create_resource_with_parent_external_id_and_no_description( self, mock_resource, capture_and_mock_http_client_request @@ -209,7 +214,7 @@ def test_create_resource_with_parent_external_id_and_no_description( self.http_client, mock_resource, 201 ) - resource = syncify( + response = syncify( self.authorization.create_resource( organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", resource_type_slug="document", @@ -234,8 +239,8 @@ def test_create_resource_with_parent_external_id_and_no_description( } assert "parent_resource_id" not in request_kwargs["json"] - assert resource.object == "authorization_resource" - assert resource.id == "res_01ABC" + assert response.object == "authorization_resource" + assert response.id == "res_01ABC" def test_create_resource_with_parent_external_id_and_none_description( self, mock_resource, capture_and_mock_http_client_request @@ -244,7 +249,7 @@ def test_create_resource_with_parent_external_id_and_none_description( self.http_client, mock_resource, 201 ) - resource = syncify( + response = syncify( self.authorization.create_resource( organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", resource_type_slug="document", @@ -271,8 +276,8 @@ def test_create_resource_with_parent_external_id_and_none_description( assert "parent_resource_id" not in request_kwargs["json"] assert "description" not in request_kwargs["json"] - assert resource.object == "authorization_resource" - assert resource.id == "res_01ABC" + assert response.object == "authorization_resource" + assert response.id == "res_01ABC" # --- update_resource --- @@ -283,7 +288,7 @@ def test_update_resource_with_name_and_description( self.http_client, mock_resource, 200 ) - resource = syncify( + response = syncify( self.authorization.update_resource( "res_01ABC", name="Updated Name", @@ -291,13 +296,13 @@ def test_update_resource_with_name_and_description( ) ) - assert resource.id == "res_01ABC" assert request_kwargs["method"] == "patch" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") assert request_kwargs["json"] == { "name": "Updated Name", "description": "Updated description", } + assert response.id == "res_01ABC" def test_update_resource_clear_description( self, mock_resource, capture_and_mock_http_client_request @@ -309,6 +314,7 @@ def test_update_resource_clear_description( syncify(self.authorization.update_resource("res_01ABC", description=None)) assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") assert request_kwargs["json"] == {"description": None} def test_update_resource_without_meta( @@ -321,6 +327,7 @@ def test_update_resource_without_meta( syncify(self.authorization.update_resource("res_01ABC")) assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") assert request_kwargs["json"] == {} def test_update_resource_without_desc( @@ -330,17 +337,17 @@ def test_update_resource_without_desc( self.http_client, mock_resource, 200 ) - resource = syncify( + response = syncify( self.authorization.update_resource( "res_01ABC", name="Updated Name", ) ) - assert resource.id == "res_01ABC" assert request_kwargs["method"] == "patch" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") assert request_kwargs["json"] == {"name": "Updated Name"} + assert response.id == "res_01ABC" # --- delete_resource --- @@ -355,10 +362,10 @@ def test_delete_resource_without_cascade( response = syncify(self.authorization.delete_resource("res_01ABC")) - assert response is None assert request_kwargs["method"] == "delete" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") assert request_kwargs.get("params") is None + assert response is None def test_delete_resource_with_cascade(self, capture_and_mock_http_client_request): request_kwargs = capture_and_mock_http_client_request( @@ -371,10 +378,10 @@ def test_delete_resource_with_cascade(self, capture_and_mock_http_client_request self.authorization.delete_resource("res_01ABC", cascade_delete=True) ) - assert response is None assert request_kwargs["method"] == "delete" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") assert request_kwargs["params"] == {"cascade_delete": "true"} + assert response is None def test_delete_resource_with_cascade_false( self, capture_and_mock_http_client_request @@ -389,7 +396,7 @@ def test_delete_resource_with_cascade_false( self.authorization.delete_resource("res_01ABC", cascade_delete=False) ) - assert response is None assert request_kwargs["method"] == "delete" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") assert request_kwargs["params"] == {"cascade_delete": "false"} + assert response is None diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py index 636d1661..5cfd0872 100644 --- a/tests/test_authorization_resource_external_id.py +++ b/tests/test_authorization_resource_external_id.py @@ -54,19 +54,23 @@ def test_get_resource_by_external_id( self.http_client, mock_resource, 200 ) - resource = syncify( + response = syncify( self.authorization.get_resource_by_external_id( MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID ) ) - assert resource.id == "res_01ABC" - assert resource.external_id == MOCK_EXTERNAL_ID - assert resource.object == "authorization_resource" assert request_kwargs["method"] == "get" assert request_kwargs["url"].endswith( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) + assert response.id == "res_01ABC" + assert response.external_id == MOCK_EXTERNAL_ID + assert response.object == "authorization_resource" + assert response.description == "A test resource for unit tests" + assert response.parent_resource_id == "res_01XYZ" + assert response.created_at == "2024-01-15T12:00:00.000Z" + assert response.updated_at == "2024-01-15T12:00:00.000Z" def test_get_resource_by_external_id_url_construction( self, mock_resource, capture_and_mock_http_client_request @@ -86,14 +90,18 @@ def test_get_resource_by_external_id_url_construction( self.http_client, mock_res, 200 ) - resource = syncify( + response = syncify( self.authorization.get_resource_by_external_id(org_id, res_type, ext_id) ) - assert resource.id == "res_02XYZ" assert request_kwargs["url"].endswith( f"/authorization/organizations/{org_id}/resources/{res_type}/{ext_id}" ) + assert response.id == "res_02XYZ" + assert response.description == "A test resource for unit tests" + assert response.parent_resource_id == "res_01XYZ" + assert response.created_at == "2024-01-15T12:00:00.000Z" + assert response.updated_at == "2024-01-15T12:00:00.000Z" # --- update_resource_by_external_id --- @@ -104,7 +112,7 @@ def test_update_resource_by_external_id_with_name( self.http_client, mock_resource, 200 ) - resource = syncify( + response = syncify( self.authorization.update_resource_by_external_id( MOCK_ORG_ID, MOCK_RESOURCE_TYPE, @@ -114,7 +122,6 @@ def test_update_resource_by_external_id_with_name( ) ) - assert resource.id == "res_01ABC" assert request_kwargs["method"] == "patch" assert request_kwargs["url"].endswith( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" @@ -123,6 +130,7 @@ def test_update_resource_by_external_id_with_name( "name": "Updated Name", "description": "Updated description", } + assert response.id == "res_01ABC" def test_update_resource_by_external_id_empty( self, mock_resource, capture_and_mock_http_client_request @@ -138,6 +146,9 @@ def test_update_resource_by_external_id_empty( ) assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith( + f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" + ) assert request_kwargs["json"] == {} def test_update_resource_by_external_id_clear_description( @@ -154,6 +165,9 @@ def test_update_resource_by_external_id_clear_description( ) assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith( + f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" + ) assert request_kwargs["json"] == {"description": None} def test_update_resource_by_external_id_without_description( @@ -163,15 +177,18 @@ def test_update_resource_by_external_id_without_description( self.http_client, mock_resource, 200 ) - resource = syncify( + response = syncify( self.authorization.update_resource_by_external_id( MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID, name="Updated Name" ) ) - assert resource.id == "res_01ABC" assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith( + f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" + ) assert request_kwargs["json"] == {"name": "Updated Name"} + assert response.id == "res_01ABC" # --- delete_resource_by_external_id --- @@ -190,12 +207,12 @@ def test_delete_resource_by_external_id_without_cascade( ) ) - assert response is None assert request_kwargs["method"] == "delete" assert request_kwargs["url"].endswith( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) assert request_kwargs.get("params") is None + assert response is None def test_delete_resource_by_external_id_with_cascade( self, capture_and_mock_http_client_request @@ -215,12 +232,12 @@ def test_delete_resource_by_external_id_with_cascade( ) ) - assert response is None assert request_kwargs["method"] == "delete" assert request_kwargs["url"].endswith( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) assert request_kwargs["params"] == {"cascade_delete": "true"} + assert response is None def test_delete_resource_by_external_id_with_cascade_false( self, capture_and_mock_http_client_request @@ -240,12 +257,12 @@ def test_delete_resource_by_external_id_with_cascade_false( ) ) - assert response is None assert request_kwargs["method"] == "delete" assert request_kwargs["url"].endswith( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) assert request_kwargs["params"] == {"cascade_delete": "false"} + assert response is None # --- list_resources --- @@ -256,15 +273,19 @@ def test_list_resources_with_results( self.http_client, mock_resources_list, 200 ) - resources_response = syncify( + response = syncify( self.authorization.list_resources(organization_id=MOCK_ORG_ID) ) assert request_kwargs["method"] == "get" assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["params"]["organization_id"] == MOCK_ORG_ID - assert len(resources_response.data) == 1 - assert resources_response.data[0].id == "res_01ABC" + assert len(response.data) == 1 + assert response.data[0].id == "res_01ABC" + assert response.data[0].description == "A test resource for unit tests" + assert response.data[0].parent_resource_id == "res_01XYZ" + assert response.data[0].created_at == "2024-01-15T12:00:00.000Z" + assert response.data[0].updated_at == "2024-01-15T12:00:00.000Z" def test_list_resources_empty_results( self, mock_resources_empty_list, capture_and_mock_http_client_request @@ -273,12 +294,13 @@ def test_list_resources_empty_results( self.http_client, mock_resources_empty_list, 200 ) - resources_response = syncify( + response = syncify( self.authorization.list_resources(organization_id=MOCK_ORG_ID) ) assert request_kwargs["method"] == "get" - assert len(resources_response.data) == 0 + assert request_kwargs["url"].endswith("/authorization/resources") + assert len(response.data) == 0 def test_list_resources_with_resource_type_slug_filter( self, mock_resources_list, capture_and_mock_http_client_request @@ -294,6 +316,7 @@ def test_list_resources_with_resource_type_slug_filter( ) assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["params"]["resource_type_slug"] == "document" def test_list_resources_with_parent_resource_id_filter( @@ -310,6 +333,7 @@ def test_list_resources_with_parent_resource_id_filter( ) assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["params"]["parent_resource_id"] == "res_01PARENT" def test_list_resources_with_parent_resource_type_slug_filter( @@ -326,6 +350,7 @@ def test_list_resources_with_parent_resource_type_slug_filter( ) assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" def test_list_resources_with_parent_external_id_filter( @@ -342,6 +367,7 @@ def test_list_resources_with_parent_external_id_filter( ) assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["params"]["parent_external_id"] == "ext_parent_456" def test_list_resources_with_search_filter( @@ -358,6 +384,7 @@ def test_list_resources_with_search_filter( ) assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["params"]["search"] == "budget" def test_list_resources_with_pagination_params( @@ -377,6 +404,8 @@ def test_list_resources_with_pagination_params( ) ) + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["params"]["limit"] == 5 assert request_kwargs["params"]["after"] == "res_cursor_abc" assert request_kwargs["params"]["before"] == "res_cursor_xyz" diff --git a/tests/test_authorization_types.py b/tests/test_authorization_types.py index 32ce0c3a..910a3f79 100644 --- a/tests/test_authorization_types.py +++ b/tests/test_authorization_types.py @@ -13,16 +13,16 @@ class TestAccessEvaluation: def test_authorized_true(self): - result = AccessCheckResponse(authorized=True) - assert result.authorized is True + response = AccessCheckResponse(authorized=True) + assert response.authorized is True def test_authorized_false(self): - result = AccessCheckResponse(authorized=False) - assert result.authorized is False + response = AccessCheckResponse(authorized=False) + assert response.authorized is False def test_from_dict(self): - result = AccessCheckResponse.model_validate({"authorized": True}) - assert result.authorized is True + response = AccessCheckResponse.model_validate({"authorized": True}) + assert response.authorized is True class TestResource: @@ -37,16 +37,16 @@ def test_resource_deserialization(self): "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", } - resource = AuthorizationResource.model_validate(data) + response = AuthorizationResource.model_validate(data) - assert resource.object == "authorization_resource" - assert resource.id == "res_01ABC" - assert resource.external_id == "ext_123" - assert resource.name == "Test Document" - assert resource.resource_type_slug == "document" - assert resource.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" - assert resource.description is None - assert resource.parent_resource_id is None + assert response.object == "authorization_resource" + assert response.id == "res_01ABC" + assert response.external_id == "ext_123" + assert response.name == "Test Document" + assert response.resource_type_slug == "document" + assert response.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" + assert response.description is None + assert response.parent_resource_id is None def test_resource_with_optional_fields(self): data = { @@ -61,10 +61,10 @@ def test_resource_with_optional_fields(self): "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", } - resource = AuthorizationResource.model_validate(data) + response = AuthorizationResource.model_validate(data) - assert resource.description == "A test document resource" - assert resource.parent_resource_id == "res_01PARENT" + assert response.description == "A test document resource" + assert response.parent_resource_id == "res_01PARENT" class TestRoleAssignment: @@ -81,14 +81,14 @@ def test_role_assignment_deserialization(self): "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", } - assignment = RoleAssignment.model_validate(data) + response = RoleAssignment.model_validate(data) - assert assignment.object == "role_assignment" - assert assignment.id == "ra_01ABC" - assert assignment.role.slug == "admin" - assert assignment.resource.id == "res_01ABC" - assert assignment.resource.external_id == "ext_123" - assert assignment.resource.resource_type_slug == "document" + assert response.object == "role_assignment" + assert response.id == "ra_01ABC" + assert response.role.slug == "admin" + assert response.resource.id == "res_01ABC" + assert response.resource.external_id == "ext_123" + assert response.resource.resource_type_slug == "document" def test_role_assignment_role(self): role = RoleAssignmentRole(slug="editor") @@ -116,10 +116,10 @@ def test_membership_deserialization(self): "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", } - membership = AuthorizationOrganizationMembership.model_validate(data) + response = AuthorizationOrganizationMembership.model_validate(data) - assert membership.object == "organization_membership" - assert membership.id == "om_01ABC" - assert membership.user_id == "user_01ABC" - assert membership.organization_id == "org_01ABC" - assert membership.status == "active" + assert response.object == "organization_membership" + assert response.id == "om_01ABC" + assert response.user_id == "user_01ABC" + assert response.organization_id == "org_01ABC" + assert response.status == "active" diff --git a/tests/utils/fixtures/mock_resource.py b/tests/utils/fixtures/mock_resource.py index ddd38957..bc03fa04 100644 --- a/tests/utils/fixtures/mock_resource.py +++ b/tests/utils/fixtures/mock_resource.py @@ -1,5 +1,3 @@ -import datetime - from workos.types.authorization.authorization_resource import AuthorizationResource @@ -9,17 +7,22 @@ def __init__( id: str = "res_01ABC", external_id: str = "ext_123", name: str = "Test Resource", + description: str = "A test resource for unit tests", resource_type_slug: str = "document", organization_id: str = "org_01EHT88Z8J8795GZNQ4ZP1J81T", + parent_resource_id: str = "res_01XYZ", + created_at: str = "2024-01-15T12:00:00.000Z", + updated_at: str = "2024-01-15T12:00:00.000Z", ): - now = datetime.datetime.now().isoformat() super().__init__( object="authorization_resource", id=id, external_id=external_id, name=name, + description=description, resource_type_slug=resource_type_slug, organization_id=organization_id, - created_at=now, - updated_at=now, + parent_resource_id=parent_resource_id, + created_at=created_at, + updated_at=updated_at, ) From 778aa671e337f3027b389de14b38409a88ae5f0c Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 04:09:42 -1000 Subject: [PATCH 23/42] get_resource --- tests/test_authorization_resource.py | 35 +++++++++++++++++-- ...test_authorization_resource_external_id.py | 8 ++--- tests/utils/fixtures/mock_resource.py | 8 +++-- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/tests/test_authorization_resource.py b/tests/test_authorization_resource.py index 112ad094..41f6d923 100644 --- a/tests/test_authorization_resource.py +++ b/tests/test_authorization_resource.py @@ -1,7 +1,7 @@ from typing import Union import pytest -from tests.utils.fixtures.mock_resource import MockResource +from tests.utils.fixtures.mock_resource import MockAuthorizationResource from tests.utils.syncify import syncify from workos.authorization import AsyncAuthorization, Authorization @@ -15,7 +15,7 @@ def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): @pytest.fixture def mock_resource(self): - return MockResource(id="res_01ABC").dict() + return MockAuthorizationResource(id="res_01ABC").dict() # --- get_resource --- @@ -40,6 +40,37 @@ def test_get_resource(self, mock_resource, capture_and_mock_http_client_request) assert response.created_at == "2024-01-15T12:00:00.000Z" assert response.updated_at == "2024-01-15T12:00:00.000Z" + def test_get_resource_without_parent(self, capture_and_mock_http_client_request): + mock_resource = MockAuthorizationResource(parent_resource_id=None).dict() + capture_and_mock_http_client_request(self.http_client, mock_resource, 200) + + response = syncify(self.authorization.get_resource("res_01ABC")) + + assert response.parent_resource_id is None + + def test_get_resource_without_description( + self, capture_and_mock_http_client_request + ): + mock_resource = MockAuthorizationResource(description=None).dict() + capture_and_mock_http_client_request(self.http_client, mock_resource, 200) + + response = syncify(self.authorization.get_resource("res_01ABC")) + + assert response.description is None + + def test_get_resource_without_parent_and_description( + self, capture_and_mock_http_client_request + ): + mock_resource = MockAuthorizationResource( + parent_resource_id=None, description=None + ).dict() + capture_and_mock_http_client_request(self.http_client, mock_resource, 200) + + response = syncify(self.authorization.get_resource("res_01ABC")) + + assert response.parent_resource_id is None + assert response.description is None + # --- create_resource --- def test_create_resource_without_parent( self, mock_resource, capture_and_mock_http_client_request diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py index 5cfd0872..c31fed06 100644 --- a/tests/test_authorization_resource_external_id.py +++ b/tests/test_authorization_resource_external_id.py @@ -1,7 +1,7 @@ from typing import Union import pytest -from tests.utils.fixtures.mock_resource import MockResource +from tests.utils.fixtures.mock_resource import MockAuthorizationResource from tests.utils.list_resource import list_response_of from tests.utils.syncify import syncify from tests.types.test_auto_pagination_function import TestAutoPaginationFunction @@ -22,7 +22,7 @@ def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): @pytest.fixture def mock_resource(self): - return MockResource( + return MockAuthorizationResource( id="res_01ABC", external_id=MOCK_EXTERNAL_ID, resource_type_slug=MOCK_RESOURCE_TYPE, @@ -40,7 +40,7 @@ def mock_resources_empty_list(self): @pytest.fixture def mock_resources_multiple(self): resources = [ - MockResource(id=f"res_{i:05d}", external_id=f"ext_{i}").dict() + MockAuthorizationResource(id=f"res_{i:05d}", external_id=f"ext_{i}").dict() for i in range(15) ] return resources @@ -79,7 +79,7 @@ def test_get_resource_by_external_id_url_construction( res_type = "folder" ext_id = "my-folder-123" - mock_res = MockResource( + mock_res = MockAuthorizationResource( id="res_02XYZ", external_id=ext_id, resource_type_slug=res_type, diff --git a/tests/utils/fixtures/mock_resource.py b/tests/utils/fixtures/mock_resource.py index bc03fa04..957a01c5 100644 --- a/tests/utils/fixtures/mock_resource.py +++ b/tests/utils/fixtures/mock_resource.py @@ -1,16 +1,18 @@ +from typing import Optional + from workos.types.authorization.authorization_resource import AuthorizationResource -class MockResource(AuthorizationResource): +class MockAuthorizationResource(AuthorizationResource): def __init__( self, id: str = "res_01ABC", external_id: str = "ext_123", name: str = "Test Resource", - description: str = "A test resource for unit tests", + description: Optional[str] = "A test resource for unit tests", resource_type_slug: str = "document", organization_id: str = "org_01EHT88Z8J8795GZNQ4ZP1J81T", - parent_resource_id: str = "res_01XYZ", + parent_resource_id: Optional[str] = "res_01XYZ", created_at: str = "2024-01-15T12:00:00.000Z", updated_at: str = "2024-01-15T12:00:00.000Z", ): From dcd8bda77d02ecb5e9e523d329a8666af6a3044b Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 04:41:00 -1000 Subject: [PATCH 24/42] create --- tests/test_async_http_client.py | 1 - tests/test_authorization_resource.py | 209 +++++++++------------------ 2 files changed, 66 insertions(+), 144 deletions(-) diff --git a/tests/test_async_http_client.py b/tests/test_async_http_client.py index 04e29d00..aadc9626 100644 --- a/tests/test_async_http_client.py +++ b/tests/test_async_http_client.py @@ -77,7 +77,6 @@ async def test_request_without_body( "method,status_code,expected_response", [ ("POST", 201, {"message": "Success!"}), - ("PUT", 200, {"message": "Success!"}), ("PATCH", 200, {"message": "Success!"}), ], ) diff --git a/tests/test_authorization_resource.py b/tests/test_authorization_resource.py index 41f6d923..0dcdc6cb 100644 --- a/tests/test_authorization_resource.py +++ b/tests/test_authorization_resource.py @@ -4,6 +4,7 @@ from tests.utils.fixtures.mock_resource import MockAuthorizationResource from tests.utils.syncify import syncify from workos.authorization import AsyncAuthorization, Authorization +from workos.exceptions import BadRequestException @pytest.mark.sync_and_async(Authorization, AsyncAuthorization) @@ -72,151 +73,99 @@ def test_get_resource_without_parent_and_description( assert response.description is None # --- create_resource --- - def test_create_resource_without_parent( + + def test_create_resource_with_parent_by_id( self, mock_resource, capture_and_mock_http_client_request ): request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 201 + self.http_client, mock_resource, 200 ) response = syncify( self.authorization.create_resource( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - resource_type_slug="document", external_id="ext_123", - name="Q4 Budget Report", - description="Financial report for Q4 2025", + name="Test Resource", + description="A test resource", + resource_type_slug="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + parent={"parent_resource_id": "res_01XYZ"}, ) ) assert request_kwargs["method"] == "post" assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["json"] == { - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "resource_type_slug": "document", + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "external_id": "ext_123", - "name": "Q4 Budget Report", - "description": "Financial report for Q4 2025", + "name": "Test Resource", + "description": "A test resource", + "parent_resource_id": "res_01XYZ", } - assert "parent_resource_id" not in request_kwargs["json"] assert "parent_resource_external_id" not in request_kwargs["json"] assert "parent_resource_type_slug" not in request_kwargs["json"] assert response.object == "authorization_resource" assert response.id == "res_01ABC" + assert response.external_id == "ext_123" + assert response.name == "Test Resource" + assert response.description == "A test resource for unit tests" + assert response.resource_type_slug == "document" + assert response.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" + assert response.parent_resource_id == "res_01XYZ" + assert response.created_at == "2024-01-15T12:00:00.000Z" + assert response.updated_at == "2024-01-15T12:00:00.000Z" - def test_create_resource_with_parent_resource_id( - self, mock_resource, capture_and_mock_http_client_request + def test_create_resource_with_parent_by_id_no_description( + self, capture_and_mock_http_client_request ): + mock_resource = MockAuthorizationResource(description=None).dict() request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 201 + self.http_client, mock_resource, 200 ) response = syncify( self.authorization.create_resource( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - resource_type_slug="document", external_id="ext_123", - name="Q4 Budget Report", - description="Financial report for Q4 2025", - parent={"parent_resource_id": "res_01PARENT"}, + name="Test Resource", + description=None, + resource_type_slug="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + parent={"parent_resource_id": "res_01XYZ"}, ) ) assert request_kwargs["method"] == "post" assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["json"] == { - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "resource_type_slug": "document", + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "external_id": "ext_123", - "name": "Q4 Budget Report", - "description": "Financial report for Q4 2025", - "parent_resource_id": "res_01PARENT", + "name": "Test Resource", + "parent_resource_id": "res_01XYZ", } + assert "description" not in request_kwargs["json"] assert "parent_resource_external_id" not in request_kwargs["json"] assert "parent_resource_type_slug" not in request_kwargs["json"] - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" + assert response.description is None - def test_create_resource_with_parent_resource_id_and_no_description( + def test_create_resource_with_parent_by_external_id( self, mock_resource, capture_and_mock_http_client_request ): request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 201 + self.http_client, mock_resource, 200 ) response = syncify( self.authorization.create_resource( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - resource_type_slug="document", external_id="ext_123", - name="Q4 Budget Report", - parent={"parent_resource_id": "res_01PARENT"}, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["json"] == { - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "resource_type_slug": "document", - "external_id": "ext_123", - "name": "Q4 Budget Report", - "parent_resource_id": "res_01PARENT", - } - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" - - def test_create_resource_with_parent_resource_id_and_none_description( - self, mock_resource, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 201 - ) - - response = syncify( - self.authorization.create_resource( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + name="Test Resource", + description="A test resource", resource_type_slug="document", - external_id="ext_123", - name="Q4 Budget Report", - description=None, - parent={"parent_resource_id": "res_01PARENT"}, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["json"] == { - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "resource_type_slug": "document", - "external_id": "ext_123", - "name": "Q4 Budget Report", - "parent_resource_id": "res_01PARENT", - } - assert "parent_resource_external_id" not in request_kwargs["json"] - assert "parent_resource_type_slug" not in request_kwargs["json"] - assert "description" not in request_kwargs["json"] - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" - - def test_create_resource_with_parent_external_id( - self, mock_resource, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 201 - ) - - response = syncify( - self.authorization.create_resource( organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - resource_type_slug="document", - external_id="ext_123", - name="Q4 Budget Report", - description="Financial report for Q4 2025", parent={ - "parent_resource_external_id": "ext_parent_456", + "parent_resource_external_id": "parent_ext_456", "parent_resource_type_slug": "folder", }, ) @@ -225,90 +174,64 @@ def test_create_resource_with_parent_external_id( assert request_kwargs["method"] == "post" assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["json"] == { - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "resource_type_slug": "document", - "external_id": "ext_123", - "name": "Q4 Budget Report", - "description": "Financial report for Q4 2025", - "parent_resource_external_id": "ext_parent_456", - "parent_resource_type_slug": "folder", - } - assert "parent_resource_id" not in request_kwargs["json"] - - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" - - def test_create_resource_with_parent_external_id_and_no_description( - self, mock_resource, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 201 - ) - - response = syncify( - self.authorization.create_resource( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - resource_type_slug="document", - external_id="ext_123", - name="Q4 Budget Report", - parent={ - "parent_resource_external_id": "ext_parent_456", - "parent_resource_type_slug": "folder", - }, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["json"] == { "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "resource_type_slug": "document", "external_id": "ext_123", - "name": "Q4 Budget Report", - "parent_resource_external_id": "ext_parent_456", + "name": "Test Resource", + "description": "A test resource", + "parent_resource_external_id": "parent_ext_456", "parent_resource_type_slug": "folder", } + assert "parent_resource_id" not in request_kwargs["json"] assert response.object == "authorization_resource" assert response.id == "res_01ABC" + assert response.external_id == "ext_123" + assert response.name == "Test Resource" + assert response.description == "A test resource for unit tests" + assert response.resource_type_slug == "document" + assert response.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" + assert response.parent_resource_id == "res_01XYZ" + assert response.created_at == "2024-01-15T12:00:00.000Z" + assert response.updated_at == "2024-01-15T12:00:00.000Z" - def test_create_resource_with_parent_external_id_and_none_description( - self, mock_resource, capture_and_mock_http_client_request + def test_create_resource_with_parent_by_external_id_no_description( + self, capture_and_mock_http_client_request ): + mock_resource = MockAuthorizationResource(description=None).dict() request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 201 + self.http_client, mock_resource, 200 ) response = syncify( self.authorization.create_resource( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - resource_type_slug="document", external_id="ext_123", - name="Q4 Budget Report", + name="Test Resource", description=None, + resource_type_slug="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", parent={ - "parent_resource_external_id": "ext_parent_456", + "parent_resource_external_id": "parent_ext_456", "parent_resource_type_slug": "folder", }, ) ) assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["json"] == { - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "resource_type_slug": "document", + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "external_id": "ext_123", - "name": "Q4 Budget Report", - "parent_resource_external_id": "ext_parent_456", + "name": "Test Resource", + "parent_resource_external_id": "parent_ext_456", "parent_resource_type_slug": "folder", } - assert "parent_resource_id" not in request_kwargs["json"] + assert "description" not in request_kwargs["json"] + assert "parent_resource_id" not in request_kwargs["json"] - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" + assert response.description is None # --- update_resource --- From 070c2ca89493b772974dd2e866fdef7511c89064 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 04:47:31 -1000 Subject: [PATCH 25/42] Get changes --- tests/test_authorization_resource.py | 73 +++++++++++++++++++--------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/tests/test_authorization_resource.py b/tests/test_authorization_resource.py index 0dcdc6cb..5e7120f8 100644 --- a/tests/test_authorization_resource.py +++ b/tests/test_authorization_resource.py @@ -234,74 +234,99 @@ def test_create_resource_with_parent_by_external_id_no_description( assert response.description is None # --- update_resource --- - - def test_update_resource_with_name_and_description( + def test_update_resource_name_only( self, mock_resource, capture_and_mock_http_client_request ): + updated_resource = MockAuthorizationResource( + name="New Name", + ).dict() request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 + self.http_client, updated_resource, 200 ) response = syncify( - self.authorization.update_resource( - "res_01ABC", - name="Updated Name", - description="Updated description", - ) + self.authorization.update_resource("res_01ABC", name="New Name") ) assert request_kwargs["method"] == "patch" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["json"] == { - "name": "Updated Name", - "description": "Updated description", - } + assert request_kwargs["json"] == {"name": "New Name"} + assert response.id == "res_01ABC" + assert response.name == "New Name" + assert response.description == "A test resource for unit tests" - def test_update_resource_clear_description( + def test_update_resource_description_only( self, mock_resource, capture_and_mock_http_client_request ): + updated_resource = MockAuthorizationResource( + description="Updated description only", + ).dict() request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 + self.http_client, updated_resource, 200 ) - syncify(self.authorization.update_resource("res_01ABC", description=None)) + response = syncify( + self.authorization.update_resource( + "res_01ABC", description="Updated description only" + ) + ) assert request_kwargs["method"] == "patch" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["json"] == {"description": None} + assert request_kwargs["json"] == { + "description": "Updated description only", + } + assert response.id == "res_01ABC" + assert response.name == "Test Resource" + assert response.description == "Updated description only" - def test_update_resource_without_meta( + def test_update_resource_remove_description( self, mock_resource, capture_and_mock_http_client_request ): + updated_resource = MockAuthorizationResource(description=None).dict() request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 + self.http_client, updated_resource, 200 ) - syncify(self.authorization.update_resource("res_01ABC")) + response = syncify( + self.authorization.update_resource("res_01ABC", description=None) + ) assert request_kwargs["method"] == "patch" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["json"] == {} + assert request_kwargs["json"] == {"description": None} + assert response.id == "res_01ABC" + assert response.description is None - def test_update_resource_without_desc( - self, mock_resource, capture_and_mock_http_client_request + def test_update_resource_with_name_and_description( + self, capture_and_mock_http_client_request ): + updated_resource = MockAuthorizationResource( + name="Updated Name", + description="Updated description", + ).dict() request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 + self.http_client, updated_resource, 200 ) response = syncify( self.authorization.update_resource( "res_01ABC", name="Updated Name", + description="Updated description", ) ) assert request_kwargs["method"] == "patch" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["json"] == {"name": "Updated Name"} + assert request_kwargs["json"] == { + "name": "Updated Name", + "description": "Updated description", + } assert response.id == "res_01ABC" + assert response.name == "Updated Name" + assert response.description == "Updated description" # --- delete_resource --- From 58564dfac63f6bc890f6d9d5369de675ea9cc1d6 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 04:50:25 -1000 Subject: [PATCH 26/42] delete --- tests/test_authorization_resource.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/test_authorization_resource.py b/tests/test_authorization_resource.py index 5e7120f8..a71cfc23 100644 --- a/tests/test_authorization_resource.py +++ b/tests/test_authorization_resource.py @@ -335,8 +335,7 @@ def test_delete_resource_without_cascade( ): request_kwargs = capture_and_mock_http_client_request( self.http_client, - status_code=202, - headers={"content-type": "text/plain; charset=utf-8"}, + status_code=204, ) response = syncify(self.authorization.delete_resource("res_01ABC")) @@ -346,11 +345,12 @@ def test_delete_resource_without_cascade( assert request_kwargs.get("params") is None assert response is None - def test_delete_resource_with_cascade(self, capture_and_mock_http_client_request): + def test_delete_resource_with_cascade_true( + self, capture_and_mock_http_client_request + ): request_kwargs = capture_and_mock_http_client_request( self.http_client, - status_code=202, - headers={"content-type": "text/plain; charset=utf-8"}, + status_code=204, ) response = syncify( @@ -367,8 +367,7 @@ def test_delete_resource_with_cascade_false( ): request_kwargs = capture_and_mock_http_client_request( self.http_client, - status_code=202, - headers={"content-type": "text/plain; charset=utf-8"}, + status_code=204, ) response = syncify( From a71ebc64e1f8de14ee88887a55c9f7abb8d1093f Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 05:25:02 -1000 Subject: [PATCH 27/42] list --- tests/test_authorization_resource.py | 369 +++++++++++++++++- ...test_authorization_resource_external_id.py | 177 --------- tests/utils/fixtures/mock_resource_list.py | 45 +++ 3 files changed, 413 insertions(+), 178 deletions(-) create mode 100644 tests/utils/fixtures/mock_resource_list.py diff --git a/tests/test_authorization_resource.py b/tests/test_authorization_resource.py index a71cfc23..a51f834a 100644 --- a/tests/test_authorization_resource.py +++ b/tests/test_authorization_resource.py @@ -1,7 +1,10 @@ from typing import Union import pytest +from tests.types.test_auto_pagination_function import TestAutoPaginationFunction from tests.utils.fixtures.mock_resource import MockAuthorizationResource +from tests.utils.fixtures.mock_resource_list import MockAuthorizationResourceList +from tests.utils.list_resource import list_response_of from tests.utils.syncify import syncify from workos.authorization import AsyncAuthorization, Authorization from workos.exceptions import BadRequestException @@ -16,7 +19,15 @@ def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): @pytest.fixture def mock_resource(self): - return MockAuthorizationResource(id="res_01ABC").dict() + return MockAuthorizationResource().dict() + + @pytest.fixture + def mock_resources_list_two(self): + return MockAuthorizationResourceList().dict() + + @pytest.fixture + def mock_resources_empty_list(self): + return list_response_of(data=[]) # --- get_resource --- @@ -378,3 +389,359 @@ def test_delete_resource_with_cascade_false( assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") assert request_kwargs["params"] == {"cascade_delete": "false"} assert response is None + + # --- list_resources --- + def test_list_resources_returns_paginated_list( + self, + mock_resources_list_two, + capture_and_mock_http_client_request, + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + response = syncify(self.authorization.list_resources()) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources") + assert request_kwargs["params"] == {"limit": 10, "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_returns_empty_list( + self, + mock_resources_empty_list, + capture_and_mock_http_client_request, + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_empty_list, 200 + ) + + response = syncify(self.authorization.list_resources()) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources") + assert request_kwargs["params"] == {"limit": 10, "order": "desc"} + + assert len(response.data) == 0 + assert response.list_metadata.before is None + assert response.list_metadata.after is None + + def test_list_resources_request_with_no_parameters( + self, + mock_resources_list_two, + capture_and_mock_http_client_request, + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify(self.authorization.list_resources()) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources") + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + assert "organization_id" not in request_kwargs["params"] + assert "resource_type_slug" not in request_kwargs["params"] + assert "parent_resource_id" not in request_kwargs["params"] + assert "parent_resource_type_slug" not in request_kwargs["params"] + assert "parent_external_id" not in request_kwargs["params"] + assert "search" not in request_kwargs["params"] + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_resources_with_organization_id( + self, mock_resources_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify(self.authorization.list_resources(organization_id="org_123")) + + assert request_kwargs["params"]["organization_id"] == "org_123" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + assert "resource_type_slug" not in request_kwargs["params"] + assert "parent_resource_id" not in request_kwargs["params"] + assert "parent_resource_type_slug" not in request_kwargs["params"] + assert "parent_external_id" not in request_kwargs["params"] + assert "search" not in request_kwargs["params"] + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_resources_with_resource_type_slug( + self, mock_resources_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify(self.authorization.list_resources(resource_type_slug="document")) + + assert request_kwargs["params"]["resource_type_slug"] == "document" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + assert "organization_id" not in request_kwargs["params"] + assert "parent_resource_id" not in request_kwargs["params"] + assert "parent_resource_type_slug" not in request_kwargs["params"] + assert "parent_external_id" not in request_kwargs["params"] + assert "search" not in request_kwargs["params"] + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_resources_with_parent_resource_id( + self, mock_resources_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify(self.authorization.list_resources(parent_resource_id="res_parent_123")) + + assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + assert "organization_id" not in request_kwargs["params"] + assert "resource_type_slug" not in request_kwargs["params"] + assert "parent_resource_type_slug" not in request_kwargs["params"] + assert "parent_external_id" not in request_kwargs["params"] + assert "search" not in request_kwargs["params"] + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_resources_with_parent_resource_type_slug( + self, mock_resources_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify(self.authorization.list_resources(parent_resource_type_slug="folder")) + + assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + assert "organization_id" not in request_kwargs["params"] + assert "resource_type_slug" not in request_kwargs["params"] + assert "parent_resource_id" not in request_kwargs["params"] + assert "parent_external_id" not in request_kwargs["params"] + assert "search" not in request_kwargs["params"] + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_resources_with_parent_external_id( + self, mock_resources_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify(self.authorization.list_resources(parent_external_id="parent_ext_456")) + + assert request_kwargs["params"]["parent_external_id"] == "parent_ext_456" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + assert "organization_id" not in request_kwargs["params"] + assert "resource_type_slug" not in request_kwargs["params"] + assert "parent_resource_id" not in request_kwargs["params"] + assert "parent_resource_type_slug" not in request_kwargs["params"] + assert "search" not in request_kwargs["params"] + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_resources_with_search( + self, mock_resources_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify(self.authorization.list_resources(search="Budget")) + + assert request_kwargs["params"]["search"] == "Budget" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + assert "organization_id" not in request_kwargs["params"] + assert "resource_type_slug" not in request_kwargs["params"] + assert "parent_resource_id" not in request_kwargs["params"] + assert "parent_resource_type_slug" not in request_kwargs["params"] + assert "parent_external_id" not in request_kwargs["params"] + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_resources_with_limit( + self, mock_resources_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify(self.authorization.list_resources(limit=25)) + + assert request_kwargs["params"]["limit"] == 25 + assert request_kwargs["params"]["order"] == "desc" + + assert "organization_id" not in request_kwargs["params"] + assert "resource_type_slug" not in request_kwargs["params"] + assert "parent_resource_id" not in request_kwargs["params"] + assert "parent_resource_type_slug" not in request_kwargs["params"] + assert "parent_external_id" not in request_kwargs["params"] + assert "search" not in request_kwargs["params"] + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_resources_with_before( + self, mock_resources_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify(self.authorization.list_resources(before="cursor_before")) + + assert request_kwargs["params"]["before"] == "cursor_before" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + assert "organization_id" not in request_kwargs["params"] + assert "resource_type_slug" not in request_kwargs["params"] + assert "parent_resource_id" not in request_kwargs["params"] + assert "parent_resource_type_slug" not in request_kwargs["params"] + assert "parent_external_id" not in request_kwargs["params"] + assert "search" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_resources_with_after( + self, mock_resources_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify(self.authorization.list_resources(after="cursor_after")) + + assert request_kwargs["params"]["after"] == "cursor_after" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + + assert "organization_id" not in request_kwargs["params"] + assert "resource_type_slug" not in request_kwargs["params"] + assert "parent_resource_id" not in request_kwargs["params"] + assert "parent_resource_type_slug" not in request_kwargs["params"] + assert "parent_external_id" not in request_kwargs["params"] + assert "search" not in request_kwargs["params"] + assert "before" not in request_kwargs["params"] + + def test_list_resources_with_order_asc( + self, mock_resources_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify(self.authorization.list_resources(order="asc")) + + assert request_kwargs["params"]["order"] == "asc" + assert request_kwargs["params"]["limit"] == 10 + + assert "organization_id" not in request_kwargs["params"] + assert "resource_type_slug" not in request_kwargs["params"] + assert "parent_resource_id" not in request_kwargs["params"] + assert "parent_resource_type_slug" not in request_kwargs["params"] + assert "parent_external_id" not in request_kwargs["params"] + assert "search" not in request_kwargs["params"] + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_resources_with_order_desc( + self, mock_resources_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify(self.authorization.list_resources(order="desc")) + + assert request_kwargs["params"]["order"] == "desc" + assert request_kwargs["params"]["limit"] == 10 + + assert "organization_id" not in request_kwargs["params"] + assert "resource_type_slug" not in request_kwargs["params"] + assert "parent_resource_id" not in request_kwargs["params"] + assert "parent_resource_type_slug" not in request_kwargs["params"] + assert "parent_external_id" not in request_kwargs["params"] + assert "search" not in request_kwargs["params"] + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_resources_with_all_parameters( + self, mock_resources_list_two, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resources_list_two, 200 + ) + + syncify( + self.authorization.list_resources( + organization_id="org_123", + resource_type_slug="document", + parent_resource_id="res_parent_123", + parent_resource_type_slug="folder", + parent_external_id="parent_ext_456", + search="Budget", + limit=5, + before="cursor_before", + after="cursor_after", + order="asc", + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources") + assert request_kwargs["params"]["organization_id"] == "org_123" + assert request_kwargs["params"]["resource_type_slug"] == "document" + assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" + assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" + assert request_kwargs["params"]["parent_external_id"] == "parent_ext_456" + assert request_kwargs["params"]["search"] == "Budget" + 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" diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py index c31fed06..2e943cd7 100644 --- a/tests/test_authorization_resource_external_id.py +++ b/tests/test_authorization_resource_external_id.py @@ -2,9 +2,7 @@ import pytest from tests.utils.fixtures.mock_resource import MockAuthorizationResource -from tests.utils.list_resource import list_response_of from tests.utils.syncify import syncify -from tests.types.test_auto_pagination_function import TestAutoPaginationFunction from workos.authorization import AsyncAuthorization, Authorization @@ -29,22 +27,6 @@ def mock_resource(self): organization_id=MOCK_ORG_ID, ).dict() - @pytest.fixture - def mock_resources_list(self, mock_resource): - return list_response_of(data=[mock_resource]) - - @pytest.fixture - def mock_resources_empty_list(self): - return list_response_of(data=[]) - - @pytest.fixture - def mock_resources_multiple(self): - resources = [ - MockAuthorizationResource(id=f"res_{i:05d}", external_id=f"ext_{i}").dict() - for i in range(15) - ] - return resources - # --- get_resource_by_external_id --- def test_get_resource_by_external_id( @@ -263,162 +245,3 @@ def test_delete_resource_by_external_id_with_cascade_false( ) assert request_kwargs["params"] == {"cascade_delete": "false"} assert response is None - - # --- list_resources --- - - def test_list_resources_with_results( - self, mock_resources_list, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - response = syncify( - self.authorization.list_resources(organization_id=MOCK_ORG_ID) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["params"]["organization_id"] == MOCK_ORG_ID - assert len(response.data) == 1 - assert response.data[0].id == "res_01ABC" - assert response.data[0].description == "A test resource for unit tests" - assert response.data[0].parent_resource_id == "res_01XYZ" - assert response.data[0].created_at == "2024-01-15T12:00:00.000Z" - assert response.data[0].updated_at == "2024-01-15T12:00:00.000Z" - - def test_list_resources_empty_results( - self, mock_resources_empty_list, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_empty_list, 200 - ) - - response = syncify( - self.authorization.list_resources(organization_id=MOCK_ORG_ID) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources") - assert len(response.data) == 0 - - def test_list_resources_with_resource_type_slug_filter( - self, mock_resources_list, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources( - organization_id=MOCK_ORG_ID, resource_type_slug="document" - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["params"]["resource_type_slug"] == "document" - - def test_list_resources_with_parent_resource_id_filter( - self, mock_resources_list, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources( - organization_id=MOCK_ORG_ID, parent_resource_id="res_01PARENT" - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["params"]["parent_resource_id"] == "res_01PARENT" - - def test_list_resources_with_parent_resource_type_slug_filter( - self, mock_resources_list, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources( - organization_id=MOCK_ORG_ID, parent_resource_type_slug="folder" - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" - - def test_list_resources_with_parent_external_id_filter( - self, mock_resources_list, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources( - organization_id=MOCK_ORG_ID, parent_external_id="ext_parent_456" - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["params"]["parent_external_id"] == "ext_parent_456" - - def test_list_resources_with_search_filter( - self, mock_resources_list, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources( - organization_id=MOCK_ORG_ID, search="budget" - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["params"]["search"] == "budget" - - def test_list_resources_with_pagination_params( - self, mock_resources_list, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources( - organization_id=MOCK_ORG_ID, - limit=5, - after="res_cursor_abc", - before="res_cursor_xyz", - order="asc", - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["params"]["limit"] == 5 - assert request_kwargs["params"]["after"] == "res_cursor_abc" - assert request_kwargs["params"]["before"] == "res_cursor_xyz" - assert request_kwargs["params"]["order"] == "asc" - - def test_list_resources_auto_pagination( - self, - mock_resources_multiple, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.authorization.list_resources, - expected_all_page_data=mock_resources_multiple, - list_function_params={"organization_id": MOCK_ORG_ID}, - ) diff --git a/tests/utils/fixtures/mock_resource_list.py b/tests/utils/fixtures/mock_resource_list.py new file mode 100644 index 00000000..a21c5c72 --- /dev/null +++ b/tests/utils/fixtures/mock_resource_list.py @@ -0,0 +1,45 @@ +from typing import Optional, Sequence + +from workos.types.authorization.authorization_resource import AuthorizationResource +from workos.types.list_resource import ListMetadata, ListPage + + +class MockAuthorizationResourceList(ListPage[AuthorizationResource]): + def __init__( + self, + data: Optional[Sequence[AuthorizationResource]] = None, + before: Optional[str] = None, + after: Optional[str] = "authz_resource_01HXYZ123ABC456DEF789DEF", + ): + if data is None: + data = [ + AuthorizationResource( + object="authorization_resource", + id="authz_resource_01HXYZ123ABC456DEF789ABC", + external_id="doc-12345678", + name="Q5 Budget Report", + description="Financial report for Q5 2025", + resource_type_slug="document", + organization_id="org_01HXYZ123ABC456DEF789ABC", + parent_resource_id="authz_resource_01HXYZ123ABC456DEF789XYZ", + created_at="2024-01-15T09:30:00.000Z", + updated_at="2024-01-15T09:30:00.000Z", + ), + AuthorizationResource( + object="authorization_resource", + id="authz_resource_01HXYZ123ABC456DEF789DEF", + external_id="folder-123", + name="Finance Folder", + description=None, + resource_type_slug="folder", + organization_id="org_01HXYZ123ABC456DEF789ABC", + parent_resource_id=None, + created_at="2024-01-14T08:00:00.000Z", + updated_at="2024-01-14T08:00:00.000Z", + ), + ] + super().__init__( + object="list", + data=data, + list_metadata=ListMetadata(before=before, after=after), + ) From 2199f799d9f4da6179e9eb14b95c89a1beb0c2c1 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 05:49:22 -1000 Subject: [PATCH 28/42] get_resource_by_external_id --- ...test_authorization_resource_external_id.py | 97 ++++++++++++------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py index 2e943cd7..b95706de 100644 --- a/tests/test_authorization_resource_external_id.py +++ b/tests/test_authorization_resource_external_id.py @@ -20,12 +20,7 @@ def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): @pytest.fixture def mock_resource(self): - return MockAuthorizationResource( - id="res_01ABC", - external_id=MOCK_EXTERNAL_ID, - resource_type_slug=MOCK_RESOURCE_TYPE, - organization_id=MOCK_ORG_ID, - ).dict() + return MockAuthorizationResource().dict() # --- get_resource_by_external_id --- @@ -46,52 +41,74 @@ def test_get_resource_by_external_id( assert request_kwargs["url"].endswith( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) + + assert response.object == "authorization_resource" assert response.id == "res_01ABC" assert response.external_id == MOCK_EXTERNAL_ID - assert response.object == "authorization_resource" + assert response.name == "Test Resource" assert response.description == "A test resource for unit tests" + assert response.resource_type_slug == "document" + assert response.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" assert response.parent_resource_id == "res_01XYZ" assert response.created_at == "2024-01-15T12:00:00.000Z" assert response.updated_at == "2024-01-15T12:00:00.000Z" - def test_get_resource_by_external_id_url_construction( - self, mock_resource, capture_and_mock_http_client_request + def test_get_resource_by_external_id_without_parent( + self, capture_and_mock_http_client_request ): - org_id = "org_different" - res_type = "folder" - ext_id = "my-folder-123" - - mock_res = MockAuthorizationResource( - id="res_02XYZ", - external_id=ext_id, - resource_type_slug=res_type, - organization_id=org_id, - ).dict() + mock_resource = MockAuthorizationResource(parent_resource_id=None).dict() + capture_and_mock_http_client_request(self.http_client, mock_resource, 200) - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_res, 200 + response = syncify( + self.authorization.get_resource_by_external_id( + MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID + ) ) + assert response.parent_resource_id is None + + def test_get_resource_by_external_id_without_description( + self, capture_and_mock_http_client_request + ): + mock_resource = MockAuthorizationResource(description=None).dict() + capture_and_mock_http_client_request(self.http_client, mock_resource, 200) + response = syncify( - self.authorization.get_resource_by_external_id(org_id, res_type, ext_id) + self.authorization.get_resource_by_external_id( + MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID + ) ) - assert request_kwargs["url"].endswith( - f"/authorization/organizations/{org_id}/resources/{res_type}/{ext_id}" + assert response.description is None + + def test_get_resource_by_external_id_without_parent_and_description( + self, capture_and_mock_http_client_request + ): + mock_resource = MockAuthorizationResource( + parent_resource_id=None, description=None + ).dict() + capture_and_mock_http_client_request(self.http_client, mock_resource, 200) + + response = syncify( + self.authorization.get_resource_by_external_id( + MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID + ) ) - assert response.id == "res_02XYZ" - assert response.description == "A test resource for unit tests" - assert response.parent_resource_id == "res_01XYZ" - assert response.created_at == "2024-01-15T12:00:00.000Z" - assert response.updated_at == "2024-01-15T12:00:00.000Z" + + assert response.parent_resource_id is None + assert response.description is None # --- update_resource_by_external_id --- def test_update_resource_by_external_id_with_name( - self, mock_resource, capture_and_mock_http_client_request + self, capture_and_mock_http_client_request ): + updated_resource = MockAuthorizationResource( + name="Updated Name", + description="Updated description", + ).dict() request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 + self.http_client, updated_resource, 200 ) response = syncify( @@ -113,6 +130,8 @@ def test_update_resource_by_external_id_with_name( "description": "Updated description", } assert response.id == "res_01ABC" + assert response.name == "Updated Name" + assert response.description == "Updated description" def test_update_resource_by_external_id_empty( self, mock_resource, capture_and_mock_http_client_request @@ -134,13 +153,14 @@ def test_update_resource_by_external_id_empty( assert request_kwargs["json"] == {} def test_update_resource_by_external_id_clear_description( - self, mock_resource, capture_and_mock_http_client_request + self, capture_and_mock_http_client_request ): + updated_resource = MockAuthorizationResource(description=None).dict() request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 + self.http_client, updated_resource, 200 ) - syncify( + response = syncify( self.authorization.update_resource_by_external_id( MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID, description=None ) @@ -151,12 +171,15 @@ def test_update_resource_by_external_id_clear_description( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) assert request_kwargs["json"] == {"description": None} + assert response.id == "res_01ABC" + assert response.description is None def test_update_resource_by_external_id_without_description( - self, mock_resource, capture_and_mock_http_client_request + self, capture_and_mock_http_client_request ): + updated_resource = MockAuthorizationResource(name="Updated Name").dict() request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 + self.http_client, updated_resource, 200 ) response = syncify( @@ -171,6 +194,8 @@ def test_update_resource_by_external_id_without_description( ) assert request_kwargs["json"] == {"name": "Updated Name"} assert response.id == "res_01ABC" + assert response.name == "Updated Name" + assert response.description == "A test resource for unit tests" # --- delete_resource_by_external_id --- From d3e3c790371d9e4a67dbed29cefdfc173c4c5085 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 05:53:52 -1000 Subject: [PATCH 29/42] update_resource_by_external_id --- ...test_authorization_resource_external_id.py | 115 ++++++++++++------ 1 file changed, 81 insertions(+), 34 deletions(-) diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py index b95706de..939b988d 100644 --- a/tests/test_authorization_resource_external_id.py +++ b/tests/test_authorization_resource_external_id.py @@ -100,24 +100,17 @@ def test_get_resource_by_external_id_without_parent_and_description( # --- update_resource_by_external_id --- - def test_update_resource_by_external_id_with_name( - self, capture_and_mock_http_client_request + def test_update_resource_by_external_id_name_only( + self, mock_resource, capture_and_mock_http_client_request ): - updated_resource = MockAuthorizationResource( - name="Updated Name", - description="Updated description", - ).dict() + updated_resource = MockAuthorizationResource(name="New Name").dict() request_kwargs = capture_and_mock_http_client_request( self.http_client, updated_resource, 200 ) response = syncify( self.authorization.update_resource_by_external_id( - MOCK_ORG_ID, - MOCK_RESOURCE_TYPE, - MOCK_EXTERNAL_ID, - name="Updated Name", - description="Updated description", + MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID, name="New Name" ) ) @@ -125,24 +118,35 @@ def test_update_resource_by_external_id_with_name( assert request_kwargs["url"].endswith( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) - assert request_kwargs["json"] == { - "name": "Updated Name", - "description": "Updated description", - } + assert request_kwargs["json"] == {"name": "New Name"} + + assert response.object == "authorization_resource" assert response.id == "res_01ABC" - assert response.name == "Updated Name" - assert response.description == "Updated description" + assert response.external_id == MOCK_EXTERNAL_ID + assert response.name == "New Name" + assert response.description == "A test resource for unit tests" + assert response.resource_type_slug == MOCK_RESOURCE_TYPE + assert response.organization_id == MOCK_ORG_ID + assert response.parent_resource_id == "res_01XYZ" + assert response.created_at == "2024-01-15T12:00:00.000Z" + assert response.updated_at == "2024-01-15T12:00:00.000Z" - def test_update_resource_by_external_id_empty( - self, mock_resource, capture_and_mock_http_client_request + def test_update_resource_by_external_id_description_only( + self, capture_and_mock_http_client_request ): + updated_resource = MockAuthorizationResource( + description="Updated description only", + ).dict() request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 + self.http_client, updated_resource, 200 ) - syncify( + response = syncify( self.authorization.update_resource_by_external_id( - MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID + MOCK_ORG_ID, + MOCK_RESOURCE_TYPE, + MOCK_EXTERNAL_ID, + description="Updated description only", ) ) @@ -150,19 +154,39 @@ def test_update_resource_by_external_id_empty( assert request_kwargs["url"].endswith( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) - assert request_kwargs["json"] == {} + assert request_kwargs["json"] == { + "description": "Updated description only", + } + + assert response.object == "authorization_resource" + assert response.id == "res_01ABC" + assert response.external_id == MOCK_EXTERNAL_ID + assert response.name == "Test Resource" + assert response.description == "Updated description only" + assert response.resource_type_slug == MOCK_RESOURCE_TYPE + assert response.organization_id == MOCK_ORG_ID + assert response.parent_resource_id == "res_01XYZ" + assert response.created_at == "2024-01-15T12:00:00.000Z" + assert response.updated_at == "2024-01-15T12:00:00.000Z" - def test_update_resource_by_external_id_clear_description( + def test_update_resource_by_external_id_name_and_description( self, capture_and_mock_http_client_request ): - updated_resource = MockAuthorizationResource(description=None).dict() + updated_resource = MockAuthorizationResource( + name="Updated Name", + description="Updated description", + ).dict() request_kwargs = capture_and_mock_http_client_request( self.http_client, updated_resource, 200 ) response = syncify( self.authorization.update_resource_by_external_id( - MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID, description=None + MOCK_ORG_ID, + MOCK_RESOURCE_TYPE, + MOCK_EXTERNAL_ID, + name="Updated Name", + description="Updated description", ) ) @@ -170,21 +194,36 @@ def test_update_resource_by_external_id_clear_description( assert request_kwargs["url"].endswith( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) - assert request_kwargs["json"] == {"description": None} + assert request_kwargs["json"] == { + "name": "Updated Name", + "description": "Updated description", + } + + assert response.object == "authorization_resource" assert response.id == "res_01ABC" - assert response.description is None + assert response.external_id == MOCK_EXTERNAL_ID + assert response.name == "Updated Name" + assert response.description == "Updated description" + assert response.resource_type_slug == MOCK_RESOURCE_TYPE + assert response.organization_id == MOCK_ORG_ID + assert response.parent_resource_id == "res_01XYZ" + assert response.created_at == "2024-01-15T12:00:00.000Z" + assert response.updated_at == "2024-01-15T12:00:00.000Z" - def test_update_resource_by_external_id_without_description( + def test_update_resource_by_external_id_remove_description( self, capture_and_mock_http_client_request ): - updated_resource = MockAuthorizationResource(name="Updated Name").dict() + updated_resource = MockAuthorizationResource(description=None).dict() request_kwargs = capture_and_mock_http_client_request( self.http_client, updated_resource, 200 ) response = syncify( self.authorization.update_resource_by_external_id( - MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID, name="Updated Name" + MOCK_ORG_ID, + MOCK_RESOURCE_TYPE, + MOCK_EXTERNAL_ID, + description=None, ) ) @@ -192,10 +231,18 @@ def test_update_resource_by_external_id_without_description( assert request_kwargs["url"].endswith( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) - assert request_kwargs["json"] == {"name": "Updated Name"} + assert request_kwargs["json"] == {"description": None} + + assert response.object == "authorization_resource" assert response.id == "res_01ABC" - assert response.name == "Updated Name" - assert response.description == "A test resource for unit tests" + assert response.external_id == MOCK_EXTERNAL_ID + assert response.name == "Test Resource" + assert response.description is None + assert response.resource_type_slug == MOCK_RESOURCE_TYPE + assert response.organization_id == MOCK_ORG_ID + assert response.parent_resource_id == "res_01XYZ" + assert response.created_at == "2024-01-15T12:00:00.000Z" + assert response.updated_at == "2024-01-15T12:00:00.000Z" # --- delete_resource_by_external_id --- From 312724132702805f6c48dceb3382e0283ecb4e74 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 05:58:54 -1000 Subject: [PATCH 30/42] delete_resource_by_external_id --- tests/test_authorization_resource_external_id.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py index 939b988d..17d74caa 100644 --- a/tests/test_authorization_resource_external_id.py +++ b/tests/test_authorization_resource_external_id.py @@ -251,8 +251,7 @@ def test_delete_resource_by_external_id_without_cascade( ): request_kwargs = capture_and_mock_http_client_request( self.http_client, - status_code=202, - headers={"content-type": "text/plain; charset=utf-8"}, + status_code=204, ) response = syncify( @@ -268,13 +267,12 @@ def test_delete_resource_by_external_id_without_cascade( assert request_kwargs.get("params") is None assert response is None - def test_delete_resource_by_external_id_with_cascade( + def test_delete_resource_by_external_id_with_cascade_true( self, capture_and_mock_http_client_request ): request_kwargs = capture_and_mock_http_client_request( self.http_client, - status_code=202, - headers={"content-type": "text/plain; charset=utf-8"}, + status_code=204, ) response = syncify( @@ -298,8 +296,7 @@ def test_delete_resource_by_external_id_with_cascade_false( ): request_kwargs = capture_and_mock_http_client_request( self.http_client, - status_code=202, - headers={"content-type": "text/plain; charset=utf-8"}, + status_code=204, ) response = syncify( From a5545488bff72ae31d1b003013e847afc6d5a8a8 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 06:10:15 -1000 Subject: [PATCH 31/42] check --- tests/test_authorization_check.py | 144 ++++++++++++++++-------------- 1 file changed, 78 insertions(+), 66 deletions(-) diff --git a/tests/test_authorization_check.py b/tests/test_authorization_check.py index 9fa1849d..2cc19e6c 100644 --- a/tests/test_authorization_check.py +++ b/tests/test_authorization_check.py @@ -8,6 +8,12 @@ ResourceIdentifierById, ) +MOCK_ORG_MEMBERSHIP_ID = "org_membership_01ABC" +MOCK_PERMISSION_SLUG = "document:read" +MOCK_RESOURCE_ID = "res_01ABC" +MOCK_RESOURCE_TYPE = "document" +MOCK_EXTERNAL_ID = "ext_123" + @pytest.mark.sync_and_async(Authorization, AsyncAuthorization) class TestAuthorizationCheck: @@ -16,115 +22,121 @@ def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): self.http_client = module_instance._http_client self.authorization = module_instance - @pytest.fixture - def mock_check_authorized(self): - return {"authorized": True} - - @pytest.fixture - def mock_check_unauthorized(self): - return {"authorized": False} - - def test_check_authorized( - self, mock_check_authorized, capture_and_mock_http_client_request + def test_check_authorized_by_resource_id( + self, capture_and_mock_http_client_request ): + mock_response = {"authorized": True} request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_check_authorized, 200 + self.http_client, mock_response, 200 ) - result = syncify( + resource: ResourceIdentifierById = {"resource_id": MOCK_RESOURCE_ID} + response = syncify( self.authorization.check( - "om_01ABC", - permission_slug="documents:read", - resource=ResourceIdentifierById(resource_id="res_01ABC"), + MOCK_ORG_MEMBERSHIP_ID, + permission_slug=MOCK_PERMISSION_SLUG, + resource=resource, ) ) - assert result.authorized is True assert request_kwargs["method"] == "post" assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/check" + f"/authorization/organization_memberships/{MOCK_ORG_MEMBERSHIP_ID}/check" ) + assert request_kwargs["json"] == { + "permission_slug": MOCK_PERMISSION_SLUG, + "resource_id": MOCK_RESOURCE_ID, + } + + assert response.authorized is True - def test_check_unauthorized( - self, mock_check_unauthorized, capture_and_mock_http_client_request + def test_check_authorized_by_external_id( + self, capture_and_mock_http_client_request ): + mock_response = {"authorized": True} request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_check_unauthorized, 200 + self.http_client, mock_response, 200 ) - result = syncify( + resource: ResourceIdentifierByExternalId = { + "resource_external_id": MOCK_EXTERNAL_ID, + "resource_type_slug": MOCK_RESOURCE_TYPE, + } + response = syncify( self.authorization.check( - "om_01ABC", - permission_slug="documents:write", - resource=ResourceIdentifierById(resource_id="res_01ABC"), + MOCK_ORG_MEMBERSHIP_ID, + permission_slug=MOCK_PERMISSION_SLUG, + resource=resource, ) ) - assert result.authorized is False assert request_kwargs["method"] == "post" - - def test_check_with_resource_id( - self, mock_check_authorized, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_check_authorized, 200 - ) - - syncify( - self.authorization.check( - "om_01ABC", - permission_slug="documents:read", - resource=ResourceIdentifierById(resource_id="res_01XYZ"), - ) + assert request_kwargs["url"].endswith( + f"/authorization/organization_memberships/{MOCK_ORG_MEMBERSHIP_ID}/check" ) - assert request_kwargs["json"] == { - "permission_slug": "documents:read", - "resource_id": "res_01XYZ", + "permission_slug": MOCK_PERMISSION_SLUG, + "resource_external_id": MOCK_EXTERNAL_ID, + "resource_type_slug": MOCK_RESOURCE_TYPE, } - assert "resource_external_id" not in request_kwargs["json"] - assert "resource_type_slug" not in request_kwargs["json"] - def test_check_with_resource_external_id( - self, mock_check_authorized, capture_and_mock_http_client_request + assert response.authorized is True + + def test_check_not_authorized_by_resource_id( + self, capture_and_mock_http_client_request ): + mock_response = {"authorized": False} request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_check_authorized, 200 + self.http_client, mock_response, 200 ) - syncify( + resource: ResourceIdentifierById = {"resource_id": MOCK_RESOURCE_ID} + response = syncify( self.authorization.check( - "om_01ABC", - permission_slug="documents:read", - resource=ResourceIdentifierByExternalId( - resource_external_id="ext_doc_123", - resource_type_slug="document", - ), + MOCK_ORG_MEMBERSHIP_ID, + permission_slug=MOCK_PERMISSION_SLUG, + resource=resource, ) ) + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith( + f"/authorization/organization_memberships/{MOCK_ORG_MEMBERSHIP_ID}/check" + ) assert request_kwargs["json"] == { - "permission_slug": "documents:read", - "resource_external_id": "ext_doc_123", - "resource_type_slug": "document", + "permission_slug": MOCK_PERMISSION_SLUG, + "resource_id": MOCK_RESOURCE_ID, } - assert "resource_id" not in request_kwargs["json"] - def test_check_url_construction( - self, mock_check_authorized, capture_and_mock_http_client_request + assert response.authorized is False + + def test_check_not_authorized_by_external_id( + self, capture_and_mock_http_client_request ): + mock_response = {"authorized": False} request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_check_authorized, 200 + self.http_client, mock_response, 200 ) - syncify( + resource: ResourceIdentifierByExternalId = { + "resource_external_id": MOCK_EXTERNAL_ID, + "resource_type_slug": MOCK_RESOURCE_TYPE, + } + response = syncify( self.authorization.check( - "om_01MEMBERSHIP", - permission_slug="admin:access", - resource=ResourceIdentifierById(resource_id="res_01ABC"), + MOCK_ORG_MEMBERSHIP_ID, + permission_slug=MOCK_PERMISSION_SLUG, + resource=resource, ) ) + assert request_kwargs["method"] == "post" assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01MEMBERSHIP/check" + f"/authorization/organization_memberships/{MOCK_ORG_MEMBERSHIP_ID}/check" ) + assert request_kwargs["json"] == { + "permission_slug": MOCK_PERMISSION_SLUG, + "resource_external_id": MOCK_EXTERNAL_ID, + "resource_type_slug": MOCK_RESOURCE_TYPE, + } + assert response.authorized is False From d7c4f7e20d6fc8f7672d053182ba09711cbc616a Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 06:23:31 -1000 Subject: [PATCH 32/42] Cleaner tests --- tests/test_authorization_resource.py | 116 +++++++++++------- ...test_authorization_resource_external_id.py | 90 ++++++-------- 2 files changed, 105 insertions(+), 101 deletions(-) diff --git a/tests/test_authorization_resource.py b/tests/test_authorization_resource.py index a51f834a..82ec0df5 100644 --- a/tests/test_authorization_resource.py +++ b/tests/test_authorization_resource.py @@ -41,16 +41,7 @@ def test_get_resource(self, mock_resource, capture_and_mock_http_client_request) assert request_kwargs["method"] == "get" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" - assert response.external_id == "ext_123" - assert response.name == "Test Resource" - assert response.description == "A test resource for unit tests" - assert response.resource_type_slug == "document" - assert response.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" - assert response.parent_resource_id == "res_01XYZ" - assert response.created_at == "2024-01-15T12:00:00.000Z" - assert response.updated_at == "2024-01-15T12:00:00.000Z" + assert response.dict() == MockAuthorizationResource().dict() def test_get_resource_without_parent(self, capture_and_mock_http_client_request): mock_resource = MockAuthorizationResource(parent_resource_id=None).dict() @@ -58,7 +49,9 @@ def test_get_resource_without_parent(self, capture_and_mock_http_client_request) response = syncify(self.authorization.get_resource("res_01ABC")) - assert response.parent_resource_id is None + assert ( + response.dict() == MockAuthorizationResource(parent_resource_id=None).dict() + ) def test_get_resource_without_description( self, capture_and_mock_http_client_request @@ -68,7 +61,7 @@ def test_get_resource_without_description( response = syncify(self.authorization.get_resource("res_01ABC")) - assert response.description is None + assert response.dict() == MockAuthorizationResource(description=None).dict() def test_get_resource_without_parent_and_description( self, capture_and_mock_http_client_request @@ -80,8 +73,12 @@ def test_get_resource_without_parent_and_description( response = syncify(self.authorization.get_resource("res_01ABC")) - assert response.parent_resource_id is None - assert response.description is None + assert ( + response.dict() + == MockAuthorizationResource( + parent_resource_id=None, description=None + ).dict() + ) # --- create_resource --- @@ -116,16 +113,7 @@ def test_create_resource_with_parent_by_id( assert "parent_resource_external_id" not in request_kwargs["json"] assert "parent_resource_type_slug" not in request_kwargs["json"] - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" - assert response.external_id == "ext_123" - assert response.name == "Test Resource" - assert response.description == "A test resource for unit tests" - assert response.resource_type_slug == "document" - assert response.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" - assert response.parent_resource_id == "res_01XYZ" - assert response.created_at == "2024-01-15T12:00:00.000Z" - assert response.updated_at == "2024-01-15T12:00:00.000Z" + assert response.dict() == MockAuthorizationResource().dict() def test_create_resource_with_parent_by_id_no_description( self, capture_and_mock_http_client_request @@ -159,7 +147,7 @@ def test_create_resource_with_parent_by_id_no_description( assert "parent_resource_external_id" not in request_kwargs["json"] assert "parent_resource_type_slug" not in request_kwargs["json"] - assert response.description is None + assert response.dict() == MockAuthorizationResource(description=None).dict() def test_create_resource_with_parent_by_external_id( self, mock_resource, capture_and_mock_http_client_request @@ -196,16 +184,7 @@ def test_create_resource_with_parent_by_external_id( assert "parent_resource_id" not in request_kwargs["json"] - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" - assert response.external_id == "ext_123" - assert response.name == "Test Resource" - assert response.description == "A test resource for unit tests" - assert response.resource_type_slug == "document" - assert response.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" - assert response.parent_resource_id == "res_01XYZ" - assert response.created_at == "2024-01-15T12:00:00.000Z" - assert response.updated_at == "2024-01-15T12:00:00.000Z" + assert response.dict() == MockAuthorizationResource().dict() def test_create_resource_with_parent_by_external_id_no_description( self, capture_and_mock_http_client_request @@ -242,7 +221,36 @@ def test_create_resource_with_parent_by_external_id_no_description( assert "description" not in request_kwargs["json"] assert "parent_resource_id" not in request_kwargs["json"] - assert response.description is None + assert response.dict() == MockAuthorizationResource(description=None).dict() + + def test_create_resource_without_parent(self, capture_and_mock_http_client_request): + mock_resource = MockAuthorizationResource(parent_resource_id=None).dict() + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + response = syncify( + self.authorization.create_resource( + external_id="ext_123", + name="Test Resource", + description="A test resource", + resource_type_slug="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + ) + ) + + assert request_kwargs["method"] == "post" + assert request_kwargs["json"] == { + "resource_type_slug": "document", + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "external_id": "ext_123", + "name": "Test Resource", + "description": "A test resource", + } + + assert ( + response.dict() == MockAuthorizationResource(parent_resource_id=None).dict() + ) # --- update_resource --- def test_update_resource_name_only( @@ -263,9 +271,12 @@ def test_update_resource_name_only( assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") assert request_kwargs["json"] == {"name": "New Name"} - assert response.id == "res_01ABC" - assert response.name == "New Name" - assert response.description == "A test resource for unit tests" + assert ( + response.dict() + == MockAuthorizationResource( + name="New Name", + ).dict() + ) def test_update_resource_description_only( self, mock_resource, capture_and_mock_http_client_request @@ -288,9 +299,12 @@ def test_update_resource_description_only( assert request_kwargs["json"] == { "description": "Updated description only", } - assert response.id == "res_01ABC" - assert response.name == "Test Resource" - assert response.description == "Updated description only" + assert ( + response.dict() + == MockAuthorizationResource( + description="Updated description only", + ).dict() + ) def test_update_resource_remove_description( self, mock_resource, capture_and_mock_http_client_request @@ -307,8 +321,12 @@ def test_update_resource_remove_description( assert request_kwargs["method"] == "patch" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") assert request_kwargs["json"] == {"description": None} - assert response.id == "res_01ABC" - assert response.description is None + assert ( + response.dict() + == MockAuthorizationResource( + description=None, + ).dict() + ) def test_update_resource_with_name_and_description( self, capture_and_mock_http_client_request @@ -335,9 +353,13 @@ def test_update_resource_with_name_and_description( "name": "Updated Name", "description": "Updated description", } - assert response.id == "res_01ABC" - assert response.name == "Updated Name" - assert response.description == "Updated description" + assert ( + response.dict() + == MockAuthorizationResource( + name="Updated Name", + description="Updated description", + ).dict() + ) # --- delete_resource --- diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py index 17d74caa..5c8011e4 100644 --- a/tests/test_authorization_resource_external_id.py +++ b/tests/test_authorization_resource_external_id.py @@ -42,16 +42,7 @@ def test_get_resource_by_external_id( f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" ) - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" - assert response.external_id == MOCK_EXTERNAL_ID - assert response.name == "Test Resource" - assert response.description == "A test resource for unit tests" - assert response.resource_type_slug == "document" - assert response.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" - assert response.parent_resource_id == "res_01XYZ" - assert response.created_at == "2024-01-15T12:00:00.000Z" - assert response.updated_at == "2024-01-15T12:00:00.000Z" + assert response.dict() == MockAuthorizationResource().dict() def test_get_resource_by_external_id_without_parent( self, capture_and_mock_http_client_request @@ -65,7 +56,9 @@ def test_get_resource_by_external_id_without_parent( ) ) - assert response.parent_resource_id is None + assert ( + response.dict() == MockAuthorizationResource(parent_resource_id=None).dict() + ) def test_get_resource_by_external_id_without_description( self, capture_and_mock_http_client_request @@ -79,7 +72,7 @@ def test_get_resource_by_external_id_without_description( ) ) - assert response.description is None + assert response.dict() == MockAuthorizationResource(description=None).dict() def test_get_resource_by_external_id_without_parent_and_description( self, capture_and_mock_http_client_request @@ -95,8 +88,12 @@ def test_get_resource_by_external_id_without_parent_and_description( ) ) - assert response.parent_resource_id is None - assert response.description is None + assert ( + response.dict() + == MockAuthorizationResource( + parent_resource_id=None, description=None + ).dict() + ) # --- update_resource_by_external_id --- @@ -120,16 +117,12 @@ def test_update_resource_by_external_id_name_only( ) assert request_kwargs["json"] == {"name": "New Name"} - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" - assert response.external_id == MOCK_EXTERNAL_ID - assert response.name == "New Name" - assert response.description == "A test resource for unit tests" - assert response.resource_type_slug == MOCK_RESOURCE_TYPE - assert response.organization_id == MOCK_ORG_ID - assert response.parent_resource_id == "res_01XYZ" - assert response.created_at == "2024-01-15T12:00:00.000Z" - assert response.updated_at == "2024-01-15T12:00:00.000Z" + assert ( + response.dict() + == MockAuthorizationResource( + name="New Name", + ).dict() + ) def test_update_resource_by_external_id_description_only( self, capture_and_mock_http_client_request @@ -158,16 +151,12 @@ def test_update_resource_by_external_id_description_only( "description": "Updated description only", } - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" - assert response.external_id == MOCK_EXTERNAL_ID - assert response.name == "Test Resource" - assert response.description == "Updated description only" - assert response.resource_type_slug == MOCK_RESOURCE_TYPE - assert response.organization_id == MOCK_ORG_ID - assert response.parent_resource_id == "res_01XYZ" - assert response.created_at == "2024-01-15T12:00:00.000Z" - assert response.updated_at == "2024-01-15T12:00:00.000Z" + assert ( + response.dict() + == MockAuthorizationResource( + description="Updated description only", + ).dict() + ) def test_update_resource_by_external_id_name_and_description( self, capture_and_mock_http_client_request @@ -199,16 +188,13 @@ def test_update_resource_by_external_id_name_and_description( "description": "Updated description", } - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" - assert response.external_id == MOCK_EXTERNAL_ID - assert response.name == "Updated Name" - assert response.description == "Updated description" - assert response.resource_type_slug == MOCK_RESOURCE_TYPE - assert response.organization_id == MOCK_ORG_ID - assert response.parent_resource_id == "res_01XYZ" - assert response.created_at == "2024-01-15T12:00:00.000Z" - assert response.updated_at == "2024-01-15T12:00:00.000Z" + assert ( + response.dict() + == MockAuthorizationResource( + name="Updated Name", + description="Updated description", + ).dict() + ) def test_update_resource_by_external_id_remove_description( self, capture_and_mock_http_client_request @@ -233,16 +219,12 @@ def test_update_resource_by_external_id_remove_description( ) assert request_kwargs["json"] == {"description": None} - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" - assert response.external_id == MOCK_EXTERNAL_ID - assert response.name == "Test Resource" - assert response.description is None - assert response.resource_type_slug == MOCK_RESOURCE_TYPE - assert response.organization_id == MOCK_ORG_ID - assert response.parent_resource_id == "res_01XYZ" - assert response.created_at == "2024-01-15T12:00:00.000Z" - assert response.updated_at == "2024-01-15T12:00:00.000Z" + assert ( + response.dict() + == MockAuthorizationResource( + description=None, + ).dict() + ) # --- delete_resource_by_external_id --- From 521175236dda937b76ba0a2cff3a87541c692553 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 06:37:06 -1000 Subject: [PATCH 33/42] nits --- src/workos/types/user_management/list_filters.py | 2 +- .../types/user_management/organization_membership.py | 9 +++++---- .../user_management/organization_membership_status.py | 3 --- src/workos/utils/_base_http_client.py | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) delete mode 100644 src/workos/types/user_management/organization_membership_status.py diff --git a/src/workos/types/user_management/list_filters.py b/src/workos/types/user_management/list_filters.py index 99d92905..a3be45ce 100644 --- a/src/workos/types/user_management/list_filters.py +++ b/src/workos/types/user_management/list_filters.py @@ -1,6 +1,6 @@ from typing import Optional, Sequence from workos.types.list_resource import ListArgs -from workos.types.user_management.organization_membership_status import ( +from workos.types.user_management.organization_membership import ( OrganizationMembershipStatus, ) diff --git a/src/workos/types/user_management/organization_membership.py b/src/workos/types/user_management/organization_membership.py index a309d562..f43ca277 100644 --- a/src/workos/types/user_management/organization_membership.py +++ b/src/workos/types/user_management/organization_membership.py @@ -3,12 +3,11 @@ from pydantic import Field from typing_extensions import TypedDict -from workos.types.user_management.organization_membership_status import ( - OrganizationMembershipStatus, -) from workos.types.workos_model import WorkOSModel from workos.typing.literals import LiteralOrUntyped +OrganizationMembershipStatus = Literal["active", "inactive", "pending"] + class BaseOrganizationMembership(WorkOSModel): object: Literal["organization_membership"] @@ -26,6 +25,8 @@ class OrganizationMembershipRole(TypedDict): class OrganizationMembership(BaseOrganizationMembership): + """Representation of an WorkOS Organization Membership.""" + role: OrganizationMembershipRole roles: Optional[Sequence[OrganizationMembershipRole]] = None - custom_attributes: Mapping[str, Any] = Field(default_factory=dict) + custom_attributes: Mapping[str, Any] = {} diff --git a/src/workos/types/user_management/organization_membership_status.py b/src/workos/types/user_management/organization_membership_status.py deleted file mode 100644 index c79384cf..00000000 --- a/src/workos/types/user_management/organization_membership_status.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Literal - -OrganizationMembershipStatus = Literal["active", "inactive", "pending"] diff --git a/src/workos/utils/_base_http_client.py b/src/workos/utils/_base_http_client.py index 402d71c2..3bcddf32 100644 --- a/src/workos/utils/_base_http_client.py +++ b/src/workos/utils/_base_http_client.py @@ -137,7 +137,7 @@ def _prepare_request( headers Optional[dict]: Custom headers to be added to the request exclude_default_auth_headers (bool): If True, excludes default auth headers from the request force_include_body (bool): If True, allows sending a body in a bodyless request (used for DELETE requests) - + exclude_none (bool): If True (default), strips keys with None values from the JSON body so only defined fields are sent. Returns: dict: Response from WorkOS """ From a6d1db9c32265cc0cd9ba49d0c42b1c8498d1a23 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 06:45:26 -1000 Subject: [PATCH 34/42] lint --- src/workos/types/list_resource.py | 3 +-- src/workos/types/user_management/organization_membership.py | 1 - tests/test_authorization_resource.py | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/workos/types/list_resource.py b/src/workos/types/list_resource.py index 5cbeed13..2c4d8894 100644 --- a/src/workos/types/list_resource.py +++ b/src/workos/types/list_resource.py @@ -33,9 +33,8 @@ from workos.types.events import Event from workos.types.feature_flags import FeatureFlag from workos.types.fga import ( - Warrant, - AuthorizationResource, AuthorizationResourceType, + Warrant, WarrantQueryResult, ) from workos.types.mfa import AuthenticationFactor diff --git a/src/workos/types/user_management/organization_membership.py b/src/workos/types/user_management/organization_membership.py index f43ca277..9009d312 100644 --- a/src/workos/types/user_management/organization_membership.py +++ b/src/workos/types/user_management/organization_membership.py @@ -1,6 +1,5 @@ from typing import Any, Literal, Mapping, Optional, Sequence -from pydantic import Field from typing_extensions import TypedDict from workos.types.workos_model import WorkOSModel diff --git a/tests/test_authorization_resource.py b/tests/test_authorization_resource.py index 82ec0df5..b08c4e91 100644 --- a/tests/test_authorization_resource.py +++ b/tests/test_authorization_resource.py @@ -1,13 +1,11 @@ from typing import Union import pytest -from tests.types.test_auto_pagination_function import TestAutoPaginationFunction from tests.utils.fixtures.mock_resource import MockAuthorizationResource from tests.utils.fixtures.mock_resource_list import MockAuthorizationResourceList from tests.utils.list_resource import list_response_of from tests.utils.syncify import syncify from workos.authorization import AsyncAuthorization, Authorization -from workos.exceptions import BadRequestException @pytest.mark.sync_and_async(Authorization, AsyncAuthorization) From 17c754c53a75676404b996a8a1e8222253c1efb6 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 06:50:24 -1000 Subject: [PATCH 35/42] type fix --- src/workos/types/list_resource.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/workos/types/list_resource.py b/src/workos/types/list_resource.py index 2c4d8894..e2f21841 100644 --- a/src/workos/types/list_resource.py +++ b/src/workos/types/list_resource.py @@ -33,6 +33,7 @@ from workos.types.events import Event from workos.types.feature_flags import FeatureFlag from workos.types.fga import ( + AuthorizationResource as FGAAuthorizationResource, AuthorizationResourceType, Warrant, WarrantQueryResult, @@ -66,7 +67,7 @@ AuthorizationResource, RoleAssignment, AuthorizationOrganizationMembership, - AuthorizationResource, + FGAAuthorizationResource, AuthorizationResourceType, User, UserManagementSession, From 7c96c593a4f6fa105d1e971c3c8215c8ef6cecef Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 06:52:41 -1000 Subject: [PATCH 36/42] type fix --- src/workos/types/user_management/organization_membership.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workos/types/user_management/organization_membership.py b/src/workos/types/user_management/organization_membership.py index 9009d312..f574736f 100644 --- a/src/workos/types/user_management/organization_membership.py +++ b/src/workos/types/user_management/organization_membership.py @@ -28,4 +28,4 @@ class OrganizationMembership(BaseOrganizationMembership): role: OrganizationMembershipRole roles: Optional[Sequence[OrganizationMembershipRole]] = None - custom_attributes: Mapping[str, Any] = {} + custom_attributes: Optional[Mapping[str, Any]] = {} From 954bb37fda3c1d2266fe0f426bab7cdd676b78b0 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 06:56:23 -1000 Subject: [PATCH 37/42] type fix --- src/workos/types/user_management/organization_membership.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/workos/types/user_management/organization_membership.py b/src/workos/types/user_management/organization_membership.py index f574736f..be90124b 100644 --- a/src/workos/types/user_management/organization_membership.py +++ b/src/workos/types/user_management/organization_membership.py @@ -14,7 +14,6 @@ class BaseOrganizationMembership(WorkOSModel): user_id: str organization_id: str status: LiteralOrUntyped[OrganizationMembershipStatus] - custom_attributes: Optional[Mapping[str, Any]] = None created_at: str updated_at: str @@ -28,4 +27,4 @@ class OrganizationMembership(BaseOrganizationMembership): role: OrganizationMembershipRole roles: Optional[Sequence[OrganizationMembershipRole]] = None - custom_attributes: Optional[Mapping[str, Any]] = {} + custom_attributes: Mapping[str, Any] = {} From 0e5eaabcffad82c41bff2ea896b514009cabfd38 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 07:33:54 -1000 Subject: [PATCH 38/42] update --- src/workos/authorization.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/workos/authorization.py b/src/workos/authorization.py index b518127f..5c3a220f 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -53,8 +53,7 @@ class ResourceListFilters(ListArgs, total=False): search: Optional[str] -# TODO RENAME -ResourcesListResource = WorkOSListResource[ +AuthorizationResourcesList = WorkOSListResource[ AuthorizationResource, ResourceListFilters, ListMetadata ] @@ -245,7 +244,7 @@ def list_resources( before: Optional[str] = None, after: Optional[str] = None, order: PaginationOrder = "desc", - ) -> SyncOrAsync[ResourcesListResource]: ... + ) -> SyncOrAsync[AuthorizationResourcesList]: ... def get_resource_by_external_id( self, @@ -646,7 +645,7 @@ def list_resources( before: Optional[str] = None, after: Optional[str] = None, order: PaginationOrder = "desc", - ) -> ResourcesListResource: + ) -> AuthorizationResourcesList: list_params: ResourceListFilters = { "limit": limit, "before": before, @@ -1122,7 +1121,7 @@ async def list_resources( before: Optional[str] = None, after: Optional[str] = None, order: PaginationOrder = "desc", - ) -> ResourcesListResource: + ) -> AuthorizationResourcesList: list_params: ResourceListFilters = { "limit": limit, "before": before, From 87eb732e5f4b239a2730941f3e97ff4537d5fe92 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Mon, 2 Mar 2026 10:35:06 -1000 Subject: [PATCH 39/42] final nit --- src/workos/fga.py | 2 + tests/test_async_http_client.py | 18 +-- ...test_authorization_resource_external_id.py | 1 - tests/test_authorization_types.py | 125 ------------------ tests/test_sync_http_client.py | 12 +- 5 files changed, 15 insertions(+), 143 deletions(-) delete mode 100644 tests/test_authorization_types.py diff --git a/src/workos/fga.py b/src/workos/fga.py index 2c76a320..4a1b7332 100644 --- a/src/workos/fga.py +++ b/src/workos/fga.py @@ -53,6 +53,8 @@ class WarrantQueryListResource( warnings: Optional[Sequence[FGAWarning]] = None +# Deprecated: Use the Authorization module instead. +# See: workos.authorization class FGAModule(Protocol): def get_resource( self, *, resource_type: str, resource_id: str diff --git a/tests/test_async_http_client.py b/tests/test_async_http_client.py index aadc9626..b842c51f 100644 --- a/tests/test_async_http_client.py +++ b/tests/test_async_http_client.py @@ -77,6 +77,7 @@ async def test_request_without_body( "method,status_code,expected_response", [ ("POST", 201, {"message": "Success!"}), + ("PUT", 200, {"message": "Success!"}), ("PATCH", 200, {"message": "Success!"}), ], ) @@ -272,7 +273,6 @@ async def test_request_includes_base_headers( await self.http_client.request("ok_place") - assert request_kwargs["url"].endswith("ok_place") default_headers = set( (header[0].lower(), header[1]) for header in self.http_client.default_headers.items() @@ -314,7 +314,6 @@ async def test_request_removes_none_parameter_values( params={"organization_id": None, "test": "value"}, ) - assert request_kwargs["url"].endswith("/test") assert request_kwargs["params"] == {"test": "value"} async def test_request_removes_none_json_values( @@ -338,12 +337,11 @@ async def test_delete_with_body_sends_json( await self.http_client.delete_with_body( path="/test", - json={"resource_id": "res_01ABC"}, + json={"obj": "json"}, ) assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith("/test") - assert request_kwargs["json"] == {"resource_id": "res_01ABC"} + assert request_kwargs["json"] == {"obj": "json"} async def test_delete_with_body_sends_params( self, capture_and_mock_http_client_request @@ -352,14 +350,12 @@ async def test_delete_with_body_sends_params( await self.http_client.delete_with_body( path="/test", - json={"resource_id": "res_01ABC"}, - params={"org_id": "org_01ABC"}, + json={"obj1": "json"}, + params={"obj2": "params"}, ) - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith("/test") - assert request_kwargs["params"] == {"org_id": "org_01ABC"} - assert request_kwargs["json"] == {"resource_id": "res_01ABC"} + assert request_kwargs["json"] == {"obj1": "json"} + assert request_kwargs["params"] == {"obj2": "params"} async def test_delete_without_body_raises_value_error(self): with pytest.raises( diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py index 5c8011e4..6cff6773 100644 --- a/tests/test_authorization_resource_external_id.py +++ b/tests/test_authorization_resource_external_id.py @@ -23,7 +23,6 @@ def mock_resource(self): return MockAuthorizationResource().dict() # --- get_resource_by_external_id --- - def test_get_resource_by_external_id( self, mock_resource, capture_and_mock_http_client_request ): diff --git a/tests/test_authorization_types.py b/tests/test_authorization_types.py deleted file mode 100644 index 910a3f79..00000000 --- a/tests/test_authorization_types.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Tests for new authorization types: AuthorizationResource, RoleAssignment, AccessEvaluation, -AuthorizationOrganizationMembership.""" - -from workos.types.authorization import ( - AccessCheckResponse, - AuthorizationOrganizationMembership, - AuthorizationResource, - RoleAssignment, - RoleAssignmentResource, - RoleAssignmentRole, -) - - -class TestAccessEvaluation: - def test_authorized_true(self): - response = AccessCheckResponse(authorized=True) - assert response.authorized is True - - def test_authorized_false(self): - response = AccessCheckResponse(authorized=False) - assert response.authorized is False - - def test_from_dict(self): - response = AccessCheckResponse.model_validate({"authorized": True}) - assert response.authorized is True - - -class TestResource: - def test_resource_deserialization(self): - data = { - "object": "authorization_resource", - "id": "res_01ABC", - "external_id": "ext_123", - "name": "Test Document", - "resource_type_slug": "document", - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T00:00:00Z", - } - response = AuthorizationResource.model_validate(data) - - assert response.object == "authorization_resource" - assert response.id == "res_01ABC" - assert response.external_id == "ext_123" - assert response.name == "Test Document" - assert response.resource_type_slug == "document" - assert response.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" - assert response.description is None - assert response.parent_resource_id is None - - def test_resource_with_optional_fields(self): - data = { - "object": "authorization_resource", - "id": "res_01ABC", - "external_id": "ext_123", - "name": "Test Document", - "description": "A test document resource", - "resource_type_slug": "document", - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "parent_resource_id": "res_01PARENT", - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T00:00:00Z", - } - response = AuthorizationResource.model_validate(data) - - assert response.description == "A test document resource" - assert response.parent_resource_id == "res_01PARENT" - - -class TestRoleAssignment: - def test_role_assignment_deserialization(self): - data = { - "object": "role_assignment", - "id": "ra_01ABC", - "role": {"slug": "admin"}, - "resource": { - "id": "res_01ABC", - "external_id": "ext_123", - "resource_type_slug": "document", - }, - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T00:00:00Z", - } - response = RoleAssignment.model_validate(data) - - assert response.object == "role_assignment" - assert response.id == "ra_01ABC" - assert response.role.slug == "admin" - assert response.resource.id == "res_01ABC" - assert response.resource.external_id == "ext_123" - assert response.resource.resource_type_slug == "document" - - def test_role_assignment_role(self): - role = RoleAssignmentRole(slug="editor") - assert role.slug == "editor" - - def test_role_assignment_resource(self): - resource = RoleAssignmentResource( - id="res_01ABC", - external_id="ext_123", - resource_type_slug="document", - ) - assert resource.id == "res_01ABC" - assert resource.external_id == "ext_123" - assert resource.resource_type_slug == "document" - - -class TestAuthorizationOrganizationMembership: - def test_membership_deserialization(self): - data = { - "object": "organization_membership", - "id": "om_01ABC", - "user_id": "user_01ABC", - "organization_id": "org_01ABC", - "status": "active", - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T00:00:00Z", - } - response = AuthorizationOrganizationMembership.model_validate(data) - - assert response.object == "organization_membership" - assert response.id == "om_01ABC" - assert response.user_id == "user_01ABC" - assert response.organization_id == "org_01ABC" - assert response.status == "active" diff --git a/tests/test_sync_http_client.py b/tests/test_sync_http_client.py index c75ae504..9023ce15 100644 --- a/tests/test_sync_http_client.py +++ b/tests/test_sync_http_client.py @@ -378,23 +378,23 @@ def test_delete_with_body_sends_json(self, capture_and_mock_http_client_request) self.http_client.delete_with_body( path="/test", - json={"resource_id": "res_01ABC"}, + json={"obj": "json"}, ) assert request_kwargs["method"] == "delete" - assert request_kwargs["json"] == {"resource_id": "res_01ABC"} + assert request_kwargs["json"] == {"obj": "json"} def test_delete_with_body_sends_params(self, capture_and_mock_http_client_request): request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) self.http_client.delete_with_body( path="/test", - json={"resource_id": "res_01ABC"}, - params={"org_id": "org_01ABC"}, + json={"obj1": "json"}, + params={"obj2": "params"}, ) - assert request_kwargs["params"] == {"org_id": "org_01ABC"} - assert request_kwargs["json"] == {"resource_id": "res_01ABC"} + assert request_kwargs["json"] == {"obj1": "json"} + assert request_kwargs["params"] == {"obj2": "params"} def test_delete_without_body_raises_value_error(self): with pytest.raises( From 01e567d6d275fffa604fc2ba53b815491198b3a0 Mon Sep 17 00:00:00 2001 From: swaroopThereItIs Date: Wed, 4 Mar 2026 05:19:42 -1000 Subject: [PATCH 40/42] FGA_4: list/assign/remove role, remove roleAssginment (#570) --- src/workos/authorization.py | 209 +++++++++++ tests/test_authorization_role_assignments.py | 358 +++++++++++++++++++ tests/utils/fixtures/mock_role_assignment.py | 68 ++++ 3 files changed, 635 insertions(+) create mode 100644 tests/test_authorization_role_assignments.py create mode 100644 tests/utils/fixtures/mock_role_assignment.py diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 5c3a220f..c32e4454 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -14,6 +14,7 @@ from workos.types.authorization.resource_identifier import ResourceIdentifier from workos.types.authorization.authorization_resource import AuthorizationResource from workos.types.authorization.role import Role, RoleList +from workos.types.authorization.role_assignment import RoleAssignment from workos.types.list_resource import ( ListArgs, ListMetadata, @@ -42,6 +43,7 @@ class _Unset(Enum): AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions" AUTHORIZATION_RESOURCES_PATH = "authorization/resources" AUTHORIZATION_ORGANIZATIONS_PATH = "authorization/organizations" +AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH = "authorization/organization_memberships" class ResourceListFilters(ListArgs, total=False): @@ -72,6 +74,15 @@ class ParentResourceByExternalId(TypedDict): _role_adapter: TypeAdapter[Role] = TypeAdapter(Role) +class RoleAssignmentListFilters(ListArgs, total=False): + organization_membership_id: str + + +RoleAssignmentsListResource = WorkOSListResource[ + RoleAssignment, RoleAssignmentListFilters, ListMetadata +] + + class PermissionListFilters(ListArgs, total=False): pass @@ -280,6 +291,38 @@ def check( resource: ResourceIdentifier, ) -> SyncOrAsync[AccessCheckResponse]: ... + def assign_role( + self, + organization_membership_id: str, + *, + role_slug: str, + resource_identifier: ResourceIdentifier, + ) -> SyncOrAsync[RoleAssignment]: ... + + def remove_role( + self, + organization_membership_id: str, + *, + role_slug: str, + resource_identifier: ResourceIdentifier, + ) -> SyncOrAsync[None]: ... + + def remove_role_assignment( + self, + organization_membership_id: str, + role_assignment_id: str, + ) -> SyncOrAsync[None]: ... + + def list_role_assignments( + self, + *, + organization_membership_id: str, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> SyncOrAsync[RoleAssignmentsListResource]: ... + class Authorization(AuthorizationModule): _http_client: SyncHTTPClient @@ -754,6 +797,89 @@ def check( return AccessCheckResponse.model_validate(response) + # Role Assignments + + def assign_role( + self, + organization_membership_id: str, + *, + role_slug: str, + resource_identifier: ResourceIdentifier, + ) -> RoleAssignment: + json: Dict[str, Any] = {"role_slug": role_slug} + json.update(resource_identifier) + + response = self._http_client.request( + f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments", + method=REQUEST_METHOD_POST, + json=json, + ) + + return RoleAssignment.model_validate(response) + + def remove_role( + self, + organization_membership_id: str, + *, + role_slug: str, + resource_identifier: ResourceIdentifier, + ) -> None: + json: Dict[str, Any] = {"role_slug": role_slug} + json.update(resource_identifier) + + self._http_client.delete_with_body( + f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments", + json=json, + ) + + def remove_role_assignment( + self, + organization_membership_id: str, + role_assignment_id: str, + ) -> None: + self._http_client.request( + f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments/{role_assignment_id}", + method=REQUEST_METHOD_DELETE, + ) + + def list_role_assignments( + self, + *, + organization_membership_id: str, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> RoleAssignmentsListResource: + list_params: RoleAssignmentListFilters = { + "organization_membership_id": organization_membership_id, + "limit": limit, + "before": before, + "after": after, + "order": order, + } + + query_params: ListArgs = { + "limit": limit, + "before": before, + "after": after, + "order": order, + } + + response = self._http_client.request( + f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments", + method=REQUEST_METHOD_GET, + params=query_params, + ) + + return WorkOSListResource[ + RoleAssignment, RoleAssignmentListFilters, ListMetadata + ]( + list_method=self.list_role_assignments, + list_args=list_params, + **ListPage[RoleAssignment](**response).model_dump(), + ) + class AsyncAuthorization(AuthorizationModule): _http_client: AsyncHTTPClient @@ -1229,3 +1355,86 @@ async def check( ) return AccessCheckResponse.model_validate(response) + + # Role Assignments + + async def assign_role( + self, + organization_membership_id: str, + *, + role_slug: str, + resource_identifier: ResourceIdentifier, + ) -> RoleAssignment: + json: Dict[str, Any] = {"role_slug": role_slug} + json.update(resource_identifier) + + response = await self._http_client.request( + f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments", + method=REQUEST_METHOD_POST, + json=json, + ) + + return RoleAssignment.model_validate(response) + + async def remove_role( + self, + organization_membership_id: str, + *, + role_slug: str, + resource_identifier: ResourceIdentifier, + ) -> None: + json: Dict[str, Any] = {"role_slug": role_slug} + json.update(resource_identifier) + + await self._http_client.delete_with_body( + f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments", + json=json, + ) + + async def remove_role_assignment( + self, + organization_membership_id: str, + role_assignment_id: str, + ) -> None: + await self._http_client.request( + f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments/{role_assignment_id}", + method=REQUEST_METHOD_DELETE, + ) + + async def list_role_assignments( + self, + *, + organization_membership_id: str, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> RoleAssignmentsListResource: + list_params: RoleAssignmentListFilters = { + "organization_membership_id": organization_membership_id, + "limit": limit, + "before": before, + "after": after, + "order": order, + } + + query_params: ListArgs = { + "limit": limit, + "before": before, + "after": after, + "order": order, + } + + response = await self._http_client.request( + f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments", + method=REQUEST_METHOD_GET, + params=query_params, + ) + + return WorkOSListResource[ + RoleAssignment, RoleAssignmentListFilters, ListMetadata + ]( + list_method=self.list_role_assignments, + list_args=list_params, + **ListPage[RoleAssignment](**response).model_dump(), + ) diff --git a/tests/test_authorization_role_assignments.py b/tests/test_authorization_role_assignments.py new file mode 100644 index 00000000..ed040ece --- /dev/null +++ b/tests/test_authorization_role_assignments.py @@ -0,0 +1,358 @@ +from typing import Union + +import pytest +from tests.utils.fixtures.mock_role_assignment import ( + MockRoleAssignment, + MockRoleAssignmentsList, +) +from tests.utils.list_resource import list_response_of +from tests.utils.syncify import syncify +from workos.authorization import AsyncAuthorization, Authorization + + +@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) +class TestAuthorizationRoleAssignments: + @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_role_assignments_list(self): + return MockRoleAssignmentsList().dict() + + @pytest.fixture + def mock_role_assignments_empty_list(self): + return list_response_of(data=[]) + + def test_assign_role_by_resource_id(self, capture_and_mock_http_client_request): + mock_role_assignment = MockRoleAssignment().dict() + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_role_assignment, 201 + ) + + response = syncify( + self.authorization.assign_role( + "om_01ABC", + role_slug="admin", + resource_identifier={"resource_id": "res_01XYZ"}, + ) + ) + + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01ABC/role_assignments" + ) + assert request_kwargs["json"] == { + "role_slug": "admin", + "resource_id": "res_01XYZ", + } + assert "resource_external_id" not in request_kwargs["json"] + assert "resource_type_slug" not in request_kwargs["json"] + + assert response.dict() == mock_role_assignment + + def test_assign_role_by_external_id_and_resource_type_slug( + self, capture_and_mock_http_client_request + ): + mock_role_assignment = MockRoleAssignment().dict() + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_role_assignment, 201 + ) + + response = syncify( + self.authorization.assign_role( + "om_01ABC", + role_slug="editor", + resource_identifier={ + "resource_external_id": "ext_doc_456", + "resource_type_slug": "document", + }, + ) + ) + + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01ABC/role_assignments" + ) + assert request_kwargs["json"] == { + "role_slug": "editor", + "resource_external_id": "ext_doc_456", + "resource_type_slug": "document", + } + assert "resource_id" not in request_kwargs["json"] + + assert response.dict() == mock_role_assignment + + def test_remove_role_by_resource_id(self, capture_and_mock_http_client_request): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, status_code=204 + ) + + syncify( + self.authorization.remove_role( + "om_01ABC", + role_slug="admin", + resource_identifier={"resource_id": "res_01XYZ"}, + ) + ) + + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01ABC/role_assignments" + ) + assert request_kwargs["json"] == { + "role_slug": "admin", + "resource_id": "res_01XYZ", + } + assert "resource_external_id" not in request_kwargs["json"] + assert "resource_type_slug" not in request_kwargs["json"] + + def test_remove_role_by_external_id_and_resource_type_slug( + self, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, status_code=204 + ) + + syncify( + self.authorization.remove_role( + "om_01ABC", + role_slug="editor", + resource_identifier={ + "resource_external_id": "ext_doc_456", + "resource_type_slug": "document", + }, + ) + ) + + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01ABC/role_assignments" + ) + assert request_kwargs["json"] == { + "role_slug": "editor", + "resource_external_id": "ext_doc_456", + "resource_type_slug": "document", + } + assert "resource_id" not in request_kwargs["json"] + + def test_remove_role_assignment(self, capture_and_mock_http_client_request): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, status_code=204 + ) + + syncify( + self.authorization.remove_role_assignment( + "om_01ABC", + role_assignment_id="ra_01XYZ", + ) + ) + + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01ABC/role_assignments/ra_01XYZ" + ) + + def test_list_role_assignments_returns_paginated_list( + self, + mock_role_assignments_list, + capture_and_mock_http_client_request, + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_role_assignments_list, 200 + ) + + response = syncify( + self.authorization.list_role_assignments( + organization_membership_id="om_01ABC" + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01ABC/role_assignments" + ) + assert request_kwargs["params"] == {"limit": 10, "order": "desc"} + + assert response.object == "list" + assert len(response.data) == 2 + + assert response.data[0].object == "role_assignment" + assert response.data[0].id == "ra_01ABC" + assert response.data[0].role.slug == "admin" + assert response.data[0].resource.id == "res_01ABC" + assert response.data[0].resource.external_id == "ext_123" + assert response.data[0].resource.resource_type_slug == "document" + 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 == "role_assignment" + assert response.data[1].id == "ra_01DEF" + assert response.data[1].role.slug == "editor" + assert response.data[1].resource.id == "res_01XYZ" + assert response.data[1].resource.external_id == "ext_456" + assert response.data[1].resource.resource_type_slug == "folder" + 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 == "ra_01DEF" + + def test_list_role_assignments_returns_empty_list( + self, + mock_role_assignments_empty_list, + capture_and_mock_http_client_request, + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_role_assignments_empty_list, 200 + ) + + response = syncify( + self.authorization.list_role_assignments( + organization_membership_id="om_01ABC" + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith( + "/authorization/organization_memberships/om_01ABC/role_assignments" + ) + assert request_kwargs["params"] == {"limit": 10, "order": "desc"} + + assert len(response.data) == 0 + assert response.list_metadata.before is None + assert response.list_metadata.after is None + + def test_list_role_assignments_with_limit( + self, + mock_role_assignments_list, + capture_and_mock_http_client_request, + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_role_assignments_list, 200 + ) + + syncify( + self.authorization.list_role_assignments( + organization_membership_id="om_01ABC", + limit=25, + ) + ) + + assert request_kwargs["params"]["limit"] == 25 + assert request_kwargs["params"]["order"] == "desc" + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_role_assignments_with_before( + self, + mock_role_assignments_list, + capture_and_mock_http_client_request, + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_role_assignments_list, 200 + ) + + syncify( + self.authorization.list_role_assignments( + organization_membership_id="om_01ABC", + before="cursor_before", + ) + ) + + assert request_kwargs["params"]["before"] == "cursor_before" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + assert "after" not in request_kwargs["params"] + + def test_list_role_assignments_with_after( + self, + mock_role_assignments_list, + capture_and_mock_http_client_request, + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_role_assignments_list, 200 + ) + + syncify( + self.authorization.list_role_assignments( + organization_membership_id="om_01ABC", + after="cursor_after", + ) + ) + + assert request_kwargs["params"]["after"] == "cursor_after" + assert request_kwargs["params"]["limit"] == 10 + assert request_kwargs["params"]["order"] == "desc" + assert "before" not in request_kwargs["params"] + + def test_list_role_assignments_with_order_desc( + self, + mock_role_assignments_list, + capture_and_mock_http_client_request, + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_role_assignments_list, 200 + ) + + syncify( + self.authorization.list_role_assignments( + organization_membership_id="om_01ABC", + order="desc", + ) + ) + + assert request_kwargs["params"]["order"] == "desc" + assert request_kwargs["params"]["limit"] == 10 + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_role_assignments_with_order_asc( + self, + mock_role_assignments_list, + capture_and_mock_http_client_request, + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_role_assignments_list, 200 + ) + + syncify( + self.authorization.list_role_assignments( + organization_membership_id="om_01ABC", + order="asc", + ) + ) + + assert request_kwargs["params"]["order"] == "asc" + assert request_kwargs["params"]["limit"] == 10 + assert "before" not in request_kwargs["params"] + assert "after" not in request_kwargs["params"] + + def test_list_role_assignments_with_all_parameters( + self, + mock_role_assignments_list, + capture_and_mock_http_client_request, + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_role_assignments_list, 200 + ) + + syncify( + self.authorization.list_role_assignments( + organization_membership_id="om_01ABC", + 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/role_assignments" + ) + 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" diff --git a/tests/utils/fixtures/mock_role_assignment.py b/tests/utils/fixtures/mock_role_assignment.py new file mode 100644 index 00000000..e9c789f0 --- /dev/null +++ b/tests/utils/fixtures/mock_role_assignment.py @@ -0,0 +1,68 @@ +from typing import Optional, Sequence + +from workos.types.authorization.role_assignment import ( + RoleAssignment, + RoleAssignmentResource, + RoleAssignmentRole, +) +from workos.types.list_resource import ListMetadata, ListPage + + +class MockRoleAssignment(RoleAssignment): + def __init__( + self, + id: str = "ra_01ABC", + role_slug: str = "admin", + resource_id: str = "res_01ABC", + resource_external_id: str = "ext_123", + resource_type_slug: str = "document", + created_at: str = "2024-01-01T00:00:00Z", + updated_at: str = "2024-01-01T00:00:00Z", + ): + super().__init__( + object="role_assignment", + id=id, + role=RoleAssignmentRole(slug=role_slug), + resource=RoleAssignmentResource( + id=resource_id, + external_id=resource_external_id, + resource_type_slug=resource_type_slug, + ), + created_at=created_at, + updated_at=updated_at, + ) + + +class MockRoleAssignmentsList(ListPage[RoleAssignment]): + def __init__( + self, + data: Optional[Sequence[RoleAssignment]] = None, + before: Optional[str] = None, + after: Optional[str] = "ra_01DEF", + ): + if data is None: + data = [ + MockRoleAssignment( + id="ra_01ABC", + role_slug="admin", + resource_id="res_01ABC", + resource_external_id="ext_123", + resource_type_slug="document", + created_at="2024-01-15T09:30:00.000Z", + updated_at="2024-01-15T09:30:00.000Z", + ), + MockRoleAssignment( + id="ra_01DEF", + role_slug="editor", + resource_id="res_01XYZ", + resource_external_id="ext_456", + resource_type_slug="folder", + created_at="2024-01-14T08:00:00.000Z", + updated_at="2024-01-14T08:00:00.000Z", + ), + ] + super().__init__( + object="list", + data=data, + list_metadata=ListMetadata(before=before, after=after), + ) From 08f1c849b0fa1d0034def5e6bbac4c3a2d322921 Mon Sep 17 00:00:00 2001 From: swaroopThereItIs Date: Wed, 4 Mar 2026 06:31:10 -1000 Subject: [PATCH 41/42] feat: FGA_5 list_resources_for_membership, list_memberships_for_resource, list_memberships_for_resource_by_external_id (#571) --- .claude/settings.local.json | 7 + src/workos/authorization.py | 316 +++++- src/workos/types/authorization/__init__.py | 3 + src/workos/types/authorization/assignment.py | 3 + .../parent_resource_identifier.py | 15 + ...test_authorization_resource_memberships.py | 904 ++++++++++++++++++ .../fixtures/mock_organization_membership.py | 42 + 7 files changed, 1278 insertions(+), 12 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 src/workos/types/authorization/assignment.py create mode 100644 src/workos/types/authorization/parent_resource_identifier.py create mode 100644 tests/test_authorization_resource_memberships.py 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() From aa664d984803a00216cb333e936d8b4d8fb541f4 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Wed, 4 Mar 2026 13:28:38 -1000 Subject: [PATCH 42/42] lol --- .claude/settings.local.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index d56caaa4..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(gh api:*)" - ] - } -}