From 37bf5f4eb079432c7597bdf78d87806354f15bab Mon Sep 17 00:00:00 2001 From: zackverham <96081108+zackverham@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:52:46 -0400 Subject: [PATCH 1/8] implements oauth association resource --- .../posit/connect/oauth/test_associations.py | 98 +++++++++++++ src/posit/connect/content.py | 9 ++ src/posit/connect/oauth/associations.py | 77 +++++++++++ src/posit/connect/oauth/integrations.py | 8 ++ .../oauth/integrations/associations.json | 10 ++ .../associations.json | 10 ++ .../posit/connect/oauth/test_associations.py | 130 ++++++++++++++++++ .../posit/connect/oauth/test_integrations.py | 15 ++ tests/posit/connect/test_content.py | 14 ++ 9 files changed, 371 insertions(+) create mode 100644 integration/tests/posit/connect/oauth/test_associations.py create mode 100644 src/posit/connect/oauth/associations.py create mode 100644 tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/oauth/integrations/associations.json create mode 100644 tests/posit/connect/__api__/v1/oauth/integrations/22644575-a27b-4118-ad06-e24459b05126/associations.json create mode 100644 tests/posit/connect/oauth/test_associations.py diff --git a/integration/tests/posit/connect/oauth/test_associations.py b/integration/tests/posit/connect/oauth/test_associations.py new file mode 100644 index 00000000..630d1cd9 --- /dev/null +++ b/integration/tests/posit/connect/oauth/test_associations.py @@ -0,0 +1,98 @@ +from pathlib import Path + +import pytest +from packaging import version + +from posit import connect + +from .. import CONNECT_VERSION + + +@pytest.mark.skipif( + CONNECT_VERSION <= version.parse("2024.06.0"), + reason="OAuth Integrations not supported.", +) +class TestAssociations: + @classmethod + def setup_class(cls): + cls.client = connect.Client() + cls.integration = cls.client.oauth.integrations.create( + name="example integration", + description="integration description", + template="custom", + config={ + "auth_mode": "Confidential", + "authorization_uri": "https://example.com/__tenand_id__/oauth2/v2.0/authorize", + "client_id": "client_id", + "client_secret": "client_secret", + "scopes": "a b c", + "token_endpoint_auth_method": "client_secret_post", + "token_uri": "https://example.com/__tenant_id__/oauth2/v2.0/token", + }, + ) + + cls.another_integration = cls.client.oauth.integrations.create( + name="another example integration", + description="another integration description", + template="custom", + config={ + "auth_mode": "Confidential", + "authorization_uri": "https://example.com/__tenand_id__/oauth2/v2.0/authorize", + "client_id": "client_id", + "client_secret": "client_secret", + "scopes": "a b c", + "token_endpoint_auth_method": "client_secret_post", + "token_uri": "https://example.com/__tenant_id__/oauth2/v2.0/token", + }, + ) + + # create content + # requires full bundle deployment to produce an interactive content type + cls.content = cls.client.content.create(name="example-flask-minimal") + # create bundle + path = Path( + "../../../../resources/connect/bundles/example-flask-minimal/bundle.tar.gz" + ) + path = (Path(__file__).parent / path).resolve() + bundle = cls.content.bundles.create(str(path)) + # deploy bundle + task = bundle.deploy() + task.wait_for() + + cls.content.oauth_associations.update(cls.integration.guid) + + @classmethod + def teardown_class(cls): + cls.integration.delete() + cls.another_integration.delete() + assert len(cls.client.oauth.integrations.find()) == 0 + + cls.content.delete() + assert cls.client.content.count() == 0 + + def test_find_by_integration(self): + associations = self.integration.associations.find() + assert len(associations) == 1 + assert associations[0].oauth_integration_guid == self.integration.guid + + no_associations = self.another_integration.associations.find() + assert len(no_associations) == 0 + + def test_find_update_by_content(self): + associations = self.content.oauth_associations.find() + assert len(associations) == 1 + assert associations[0].app_guid == self.content.guid + assert associations[0].oauth_integration_guid == self.integration.guid + + # update content association to another_integration + self.content.oauth_associations.update(self.another_integration.guid) + updated_associations = self.content.oauth_associations.find() + assert len(updated_associations) == 1 + assert updated_associations[0].app_guid == self.content.guid + assert updated_associations[0].oauth_integration_guid == self.another_integration.guid + + # unset content association + self.content.oauth_associations.update(None) + no_associations = self.content.oauth_associations.find() + assert len(no_associations) == 0 + diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py index 891591ce..06c04896 100644 --- a/src/posit/connect/content.py +++ b/src/posit/connect/content.py @@ -7,6 +7,8 @@ from posixpath import dirname from typing import Any, List, Optional, overload +from posit.connect.oauth.associations import ContentItemAssociations + from . import tasks from .bundles import Bundles from .env import EnvVars @@ -27,6 +29,13 @@ def __getitem__(self, key: Any) -> Any: return ContentItemOwner(params=self.params, **v) return v + @property + def oauth_associations(self) -> ContentItemAssociations: + if self.guid is None: + raise ValueError("ContentItem must have a guid to have associations") + return ContentItemAssociations(self.params, content_guid=self.guid) + + def delete(self) -> None: """Delete the content item.""" path = f"v1/content/{self.guid}" diff --git a/src/posit/connect/oauth/associations.py b/src/posit/connect/oauth/associations.py new file mode 100644 index 00000000..147e1873 --- /dev/null +++ b/src/posit/connect/oauth/associations.py @@ -0,0 +1,77 @@ +"""OAuth association resources.""" + +from typing import List + +from ..resources import Resource, ResourceParameters, Resources + + +class Association(Resource): + pass + + +class IntegrationAssociations(Resources): + """IntegrationAssociations resource.""" + + def __init__(self, params: ResourceParameters, integration_guid: str) -> None: + super().__init__(params) + self.integration_guid = integration_guid + + def find(self) -> List[Association]: + """Find OAuth associations. + + Returns + ------- + List[Association] + """ + path = f"v1/oauth/integrations/{self.integration_guid}/associations" + url = self.params.url + path + + response = self.params.session.get(url) + return [ + Association( + self.params, + **result, + ) + for result in response.json() + ] + + +class ContentItemAssociations(Resources): + """ContentItemAssociations resource.""" + + def __init__(self, params: ResourceParameters, content_guid: str) -> None: + super().__init__(params) + self.content_guid = content_guid + + + def find(self) -> List[Association]: + """Find OAuth associations. + + Returns + ------- + List[Association] + """ + path = f"v1/content/{self.content_guid}/oauth/integrations/associations" + url = self.params.url + path + response = self.params.session.get(url) + return [ + Association( + self.params, + **result, + ) + for result in response.json() + ] + + + def update(self, integration_guid: None | str) -> None: + """Set integration associations""" + + + if integration_guid is None: + data = [] + else: + data = [{"oauth_integration_guid": integration_guid}] + + path = f"v1/content/{self.content_guid}/oauth/integrations/associations" + url = self.params.url + path + self.params.session.put(url, json=data) diff --git a/src/posit/connect/oauth/integrations.py b/src/posit/connect/oauth/integrations.py index b0ddb794..d2e1514b 100644 --- a/src/posit/connect/oauth/integrations.py +++ b/src/posit/connect/oauth/integrations.py @@ -2,12 +2,20 @@ from typing import List, Optional, overload +from posit.connect.oauth.associations import IntegrationAssociations + from ..resources import Resource, Resources class Integration(Resource): """OAuth integration resource.""" + @property + def associations(self) -> IntegrationAssociations: + if self.guid is None: + raise ValueError("Integration must have a guid to have associations") + return IntegrationAssociations(self.params, integration_guid=self.guid) + def delete(self) -> None: """Delete the OAuth integration.""" path = f"v1/oauth/integrations/{self['guid']}" diff --git a/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/oauth/integrations/associations.json b/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/oauth/integrations/associations.json new file mode 100644 index 00000000..b669a758 --- /dev/null +++ b/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/oauth/integrations/associations.json @@ -0,0 +1,10 @@ +[ + { + "app_guid": "f2f37341-e21d-3d80-c698-a935ad614066", + "oauth_integration_guid": "22644575-a27b-4118-ad06-e24459b05126", + "oauth_integration_name": "keycloak integration", + "oauth_integration_description": "integration description", + "oauth_integration_template": "custom", + "created_time": "2024-10-01T18:16:09Z" + } +] diff --git a/tests/posit/connect/__api__/v1/oauth/integrations/22644575-a27b-4118-ad06-e24459b05126/associations.json b/tests/posit/connect/__api__/v1/oauth/integrations/22644575-a27b-4118-ad06-e24459b05126/associations.json new file mode 100644 index 00000000..b669a758 --- /dev/null +++ b/tests/posit/connect/__api__/v1/oauth/integrations/22644575-a27b-4118-ad06-e24459b05126/associations.json @@ -0,0 +1,10 @@ +[ + { + "app_guid": "f2f37341-e21d-3d80-c698-a935ad614066", + "oauth_integration_guid": "22644575-a27b-4118-ad06-e24459b05126", + "oauth_integration_name": "keycloak integration", + "oauth_integration_description": "integration description", + "oauth_integration_template": "custom", + "created_time": "2024-10-01T18:16:09Z" + } +] diff --git a/tests/posit/connect/oauth/test_associations.py b/tests/posit/connect/oauth/test_associations.py new file mode 100644 index 00000000..d0d91144 --- /dev/null +++ b/tests/posit/connect/oauth/test_associations.py @@ -0,0 +1,130 @@ +from unittest import mock + +import responses + +from posit.connect.client import Client +from posit.connect.oauth.associations import Association + +from ..api import load_mock + + +class TestAssociationAttributes: + @classmethod + def setup_class(cls): + guid = "22644575-a27b-4118-ad06-e24459b05126" + fake_items = load_mock(f"v1/oauth/integrations/{guid}/associations.json") + + assert len(fake_items) == 1 + fake_item = fake_items[0] + + cls.item = Association(mock.Mock(), **fake_item) + + def test_app_guid(self): + assert self.item.app_guid == "f2f37341-e21d-3d80-c698-a935ad614066" + + def test_oauth_integration_guid(self): + assert self.item.oauth_integration_guid == "22644575-a27b-4118-ad06-e24459b05126" + + def test_oauth_integration_name(self): + assert self.item.oauth_integration_name == "keycloak integration" + + def test_oauth_integration_description(self): + assert self.item.oauth_integration_description == "integration description" + + def test_oauth_integration_template(self): + assert self.item.oauth_integration_template == "custom" + + def test_created_time(self): + assert self.item.created_time == "2024-10-01T18:16:09Z" + +class TestIntegrationAssociationsFind: + @responses.activate + def test(self): + guid = "22644575-a27b-4118-ad06-e24459b05126" + + # behavior + mock_get_integration = responses.get( + f"https://connect.example/__api__/v1/oauth/integrations/{guid}", + json=load_mock(f"v1/oauth/integrations/{guid}.json"), + ) + mock_get_association = responses.get( + f"https://connect.example/__api__/v1/oauth/integrations/{guid}/associations", + json=load_mock(f"v1/oauth/integrations/{guid}/associations.json"), + ) + + + # setup + c = Client("https://connect.example", "12345") + # invoke + associations = c.oauth.integrations.get(guid).associations.find() + + assert len(associations) == 1 + association = associations[0] + assert association.oauth_integration_guid == guid + + + assert mock_get_integration.call_count == 1 + assert mock_get_association.call_count == 1 + + +class TestContentAssociationsFind: + @responses.activate + def test(self): + guid = "f2f37341-e21d-3d80-c698-a935ad614066" + + # behavior + mock_get_content = responses.get( + f"https://connect.example/__api__/v1/content/{guid}", + json=load_mock(f"v1/content/{guid}.json"), + ) + mock_get_association = responses.get( + f"https://connect.example/__api__/v1/content/{guid}/oauth/integrations/associations", + json=load_mock(f"v1/content/{guid}/oauth/integrations/associations.json"), + ) + + + # setup + c = Client("https://connect.example", "12345") + # invoke + associations = c.content.get(guid).oauth_associations.find() + assert len(associations) == 1 + association = associations[0] + assert association.app_guid == guid + + + assert mock_get_content.call_count == 1 + assert mock_get_association.call_count == 1 + + + +class TestContentAssociationsUpdate: + @responses.activate + def test_single(self): + guid = "f2f37341-e21d-3d80-c698-a935ad614066" + + # behavior + mock_get_content = responses.get( + f"https://connect.example/__api__/v1/content/{guid}", + json=load_mock(f"v1/content/{guid}.json"), + ) + + new_integration_guid = "00000000-a27b-4118-ad06-e24459b05126" + + mock_put = responses.put( + f"https://connect.example/__api__/v1/content/{guid}/oauth/integrations/associations", + json = [{"oauth_integration_guid": new_integration_guid}] + ) + + # setup + c = Client("https://connect.example", "12345") + + # invoke + c.content.get(guid).oauth_associations.update(new_integration_guid) + + # assert + assert mock_put.call_count == 1 + assert mock_get_content.call_count == 1 + + + + diff --git a/tests/posit/connect/oauth/test_integrations.py b/tests/posit/connect/oauth/test_integrations.py index 67049ff2..fd4d5bf4 100644 --- a/tests/posit/connect/oauth/test_integrations.py +++ b/tests/posit/connect/oauth/test_integrations.py @@ -1,9 +1,11 @@ from unittest import mock +import pytest import responses from responses import matchers from posit.connect.client import Client +from posit.connect.oauth.associations import IntegrationAssociations from posit.connect.oauth.integrations import Integration from ..api import load_mock # type: ignore @@ -54,6 +56,19 @@ def test_created_time(self): def test_updated_time(self): assert self.item.updated_time == "2024-07-17T19:28:05Z" + def test_associations(self): + assert isinstance(self.item.associations, IntegrationAssociations) + +class TestIntegrationOAuthAssociationsError: + def test(self): + fake_item = load_mock( + "v1/oauth/integrations/22644575-a27b-4118-ad06-e24459b05126.json" + ) + del fake_item["guid"] + integration = Integration(mock.Mock(), **fake_item) + + with pytest.raises(ValueError): + integration.associations class TestIntegrationDelete: @responses.activate diff --git a/tests/posit/connect/test_content.py b/tests/posit/connect/test_content.py index 784f1c00..f4f27580 100644 --- a/tests/posit/connect/test_content.py +++ b/tests/posit/connect/test_content.py @@ -6,6 +6,7 @@ from posit.connect.client import Client from posit.connect.content import ContentItem, ContentItemOwner +from posit.connect.oauth.associations import ContentItemAssociations from posit.connect.permissions import Permissions from .api import load_mock # type: ignore @@ -178,10 +179,23 @@ def test_owner(self): def test_permissions(self): assert isinstance(self.item.permissions, Permissions) + + def test_oauth_associations(self): + assert isinstance(self.item.oauth_associations, ContentItemAssociations) def test_tags(self): assert self.item.tags is None +class TestContentItemOAuthAssociationsError: + def test(self): + fake_item = load_mock( + "v1/content/f2f37341-e21d-3d80-c698-a935ad614066.json" + ) + del fake_item["guid"] + content_item = ContentItem(mock.Mock(), **fake_item) + + with pytest.raises(ValueError): + content_item.oauth_associations class TestContentItemGetContentOwner: @responses.activate From ff92391f1fd59b7db369bac610114b5f3e178da9 Mon Sep 17 00:00:00 2001 From: zackverham <96081108+zackverham@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:22:33 -0400 Subject: [PATCH 2/8] responses / improvements from PR comments --- .../posit/connect/oauth/test_associations.py | 36 ++++++++----- src/posit/connect/content.py | 23 +++++--- src/posit/connect/oauth/associations.py | 24 +++++---- src/posit/connect/oauth/integrations.py | 10 ++-- .../posit/connect/oauth/test_associations.py | 52 ++++++++++--------- .../posit/connect/oauth/test_integrations.py | 6 ++- tests/posit/connect/test_content.py | 23 +++++--- 7 files changed, 109 insertions(+), 65 deletions(-) diff --git a/integration/tests/posit/connect/oauth/test_associations.py b/integration/tests/posit/connect/oauth/test_associations.py index 630d1cd9..d73a37e6 100644 --- a/integration/tests/posit/connect/oauth/test_associations.py +++ b/integration/tests/posit/connect/oauth/test_associations.py @@ -58,8 +58,8 @@ def setup_class(cls): # deploy bundle task = bundle.deploy() task.wait_for() - - cls.content.oauth_associations.update(cls.integration.guid) + + cls.content.oauth.associations.update(cls.integration["guid"]) @classmethod def teardown_class(cls): @@ -73,26 +73,36 @@ def teardown_class(cls): def test_find_by_integration(self): associations = self.integration.associations.find() assert len(associations) == 1 - assert associations[0].oauth_integration_guid == self.integration.guid + assert ( + associations[0]["oauth_integration_guid"] + == self.integration["guid"] + ) no_associations = self.another_integration.associations.find() assert len(no_associations) == 0 def test_find_update_by_content(self): - associations = self.content.oauth_associations.find() + associations = self.content.oauth.associations.find() assert len(associations) == 1 - assert associations[0].app_guid == self.content.guid - assert associations[0].oauth_integration_guid == self.integration.guid + assert associations[0]["app_guid"] == self.content["guid"] + assert ( + associations[0]["oauth_integration_guid"] + == self.integration["guid"] + ) # update content association to another_integration - self.content.oauth_associations.update(self.another_integration.guid) - updated_associations = self.content.oauth_associations.find() + self.content.oauth.associations.update( + self.another_integration["guid"] + ) + updated_associations = self.content.oauth.associations.find() assert len(updated_associations) == 1 - assert updated_associations[0].app_guid == self.content.guid - assert updated_associations[0].oauth_integration_guid == self.another_integration.guid + assert updated_associations[0]["app_guid"] == self.content["guid"] + assert ( + updated_associations[0]["oauth_integration_guid"] + == self.another_integration.guid + ) # unset content association - self.content.oauth_associations.update(None) - no_associations = self.content.oauth_associations.find() + self.content.oauth.associations.update(None) + no_associations = self.content.oauth.associations.find() assert len(no_associations) == 0 - diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py index 06c04896..9956249a 100644 --- a/src/posit/connect/content.py +++ b/src/posit/connect/content.py @@ -7,7 +7,7 @@ from posixpath import dirname from typing import Any, List, Optional, overload -from posit.connect.oauth.associations import ContentItemAssociations +from posit.connect.oauth.associations import ContentItemAssociations from . import tasks from .bundles import Bundles @@ -18,6 +18,18 @@ from .variants import Variants +class ContentItemOAuth(Resource): + def __init__(self, params: ResourceParameters, content_guid: str) -> None: + super().__init__(params) + self.content_guid = content_guid + + @property + def associations(self) -> ContentItemAssociations: + return ContentItemAssociations( + self.params, content_guid=self.content_guid + ) + + class ContentItemOwner(Resource): pass @@ -30,11 +42,10 @@ def __getitem__(self, key: Any) -> Any: return v @property - def oauth_associations(self) -> ContentItemAssociations: - if self.guid is None: - raise ValueError("ContentItem must have a guid to have associations") - return ContentItemAssociations(self.params, content_guid=self.guid) - + def oauth(self) -> ContentItemOAuth: + if "guid" not in self: + raise ValueError("ContentItemOAuth requires content guid") + return ContentItemOAuth(self.params, content_guid=self["guid"]) def delete(self) -> None: """Delete the content item.""" diff --git a/src/posit/connect/oauth/associations.py b/src/posit/connect/oauth/associations.py index 147e1873..0fef4ea5 100644 --- a/src/posit/connect/oauth/associations.py +++ b/src/posit/connect/oauth/associations.py @@ -1,6 +1,6 @@ """OAuth association resources.""" -from typing import List +from typing import List from ..resources import Resource, ResourceParameters, Resources @@ -12,9 +12,11 @@ class Association(Resource): class IntegrationAssociations(Resources): """IntegrationAssociations resource.""" - def __init__(self, params: ResourceParameters, integration_guid: str) -> None: + def __init__( + self, params: ResourceParameters, integration_guid: str + ) -> None: super().__init__(params) - self.integration_guid = integration_guid + self.integration_guid = integration_guid def find(self) -> List[Association]: """Find OAuth associations. @@ -40,10 +42,9 @@ class ContentItemAssociations(Resources): """ContentItemAssociations resource.""" def __init__(self, params: ResourceParameters, content_guid: str) -> None: - super().__init__(params) + super().__init__(params) self.content_guid = content_guid - def find(self) -> List[Association]: """Find OAuth associations. @@ -51,7 +52,9 @@ def find(self) -> List[Association]: ------- List[Association] """ - path = f"v1/content/{self.content_guid}/oauth/integrations/associations" + path = ( + f"v1/content/{self.content_guid}/oauth/integrations/associations" + ) url = self.params.url + path response = self.params.session.get(url) return [ @@ -62,16 +65,15 @@ def find(self) -> List[Association]: for result in response.json() ] - def update(self, integration_guid: None | str) -> None: - """Set integration associations""" - - + """Set integration associations.""" if integration_guid is None: data = [] else: data = [{"oauth_integration_guid": integration_guid}] - path = f"v1/content/{self.content_guid}/oauth/integrations/associations" + path = ( + f"v1/content/{self.content_guid}/oauth/integrations/associations" + ) url = self.params.url + path self.params.session.put(url, json=data) diff --git a/src/posit/connect/oauth/integrations.py b/src/posit/connect/oauth/integrations.py index d2e1514b..3ebc48f2 100644 --- a/src/posit/connect/oauth/integrations.py +++ b/src/posit/connect/oauth/integrations.py @@ -12,9 +12,13 @@ class Integration(Resource): @property def associations(self) -> IntegrationAssociations: - if self.guid is None: - raise ValueError("Integration must have a guid to have associations") - return IntegrationAssociations(self.params, integration_guid=self.guid) + if "guid" not in self: + raise ValueError( + "IntegrationAssociations requires integration guid" + ) + return IntegrationAssociations( + self.params, integration_guid=self["guid"] + ) def delete(self) -> None: """Delete the OAuth integration.""" diff --git a/tests/posit/connect/oauth/test_associations.py b/tests/posit/connect/oauth/test_associations.py index d0d91144..d8a5d699 100644 --- a/tests/posit/connect/oauth/test_associations.py +++ b/tests/posit/connect/oauth/test_associations.py @@ -12,30 +12,39 @@ class TestAssociationAttributes: @classmethod def setup_class(cls): guid = "22644575-a27b-4118-ad06-e24459b05126" - fake_items = load_mock(f"v1/oauth/integrations/{guid}/associations.json") + fake_items = load_mock( + f"v1/oauth/integrations/{guid}/associations.json" + ) assert len(fake_items) == 1 fake_item = fake_items[0] - + cls.item = Association(mock.Mock(), **fake_item) def test_app_guid(self): - assert self.item.app_guid == "f2f37341-e21d-3d80-c698-a935ad614066" + assert self.item["app_guid"] == "f2f37341-e21d-3d80-c698-a935ad614066" def test_oauth_integration_guid(self): - assert self.item.oauth_integration_guid == "22644575-a27b-4118-ad06-e24459b05126" + assert ( + self.item["oauth_integration_guid"] + == "22644575-a27b-4118-ad06-e24459b05126" + ) def test_oauth_integration_name(self): - assert self.item.oauth_integration_name == "keycloak integration" + assert self.item["oauth_integration_name"] == "keycloak integration" def test_oauth_integration_description(self): - assert self.item.oauth_integration_description == "integration description" + assert ( + self.item["oauth_integration_description"] + == "integration description" + ) def test_oauth_integration_template(self): - assert self.item.oauth_integration_template == "custom" + assert self.item["oauth_integration_template"] == "custom" def test_created_time(self): - assert self.item.created_time == "2024-10-01T18:16:09Z" + assert self.item["created_time"] == "2024-10-01T18:16:09Z" + class TestIntegrationAssociationsFind: @responses.activate @@ -52,7 +61,6 @@ def test(self): json=load_mock(f"v1/oauth/integrations/{guid}/associations.json"), ) - # setup c = Client("https://connect.example", "12345") # invoke @@ -60,8 +68,7 @@ def test(self): assert len(associations) == 1 association = associations[0] - assert association.oauth_integration_guid == guid - + assert association["oauth_integration_guid"] == guid assert mock_get_integration.call_count == 1 assert mock_get_association.call_count == 1 @@ -79,24 +86,25 @@ def test(self): ) mock_get_association = responses.get( f"https://connect.example/__api__/v1/content/{guid}/oauth/integrations/associations", - json=load_mock(f"v1/content/{guid}/oauth/integrations/associations.json"), + json=load_mock( + f"v1/content/{guid}/oauth/integrations/associations.json" + ), ) - # setup c = Client("https://connect.example", "12345") # invoke - associations = c.content.get(guid).oauth_associations.find() + associations = c.content.get(guid).oauth.associations.find() + + # assert assert len(associations) == 1 association = associations[0] - assert association.app_guid == guid - + assert association["app_guid"] == guid assert mock_get_content.call_count == 1 assert mock_get_association.call_count == 1 - class TestContentAssociationsUpdate: @responses.activate def test_single(self): @@ -112,19 +120,15 @@ def test_single(self): mock_put = responses.put( f"https://connect.example/__api__/v1/content/{guid}/oauth/integrations/associations", - json = [{"oauth_integration_guid": new_integration_guid}] + json=[{"oauth_integration_guid": new_integration_guid}], ) # setup c = Client("https://connect.example", "12345") - + # invoke - c.content.get(guid).oauth_associations.update(new_integration_guid) + c.content.get(guid).oauth.associations.update(new_integration_guid) # assert assert mock_put.call_count == 1 assert mock_get_content.call_count == 1 - - - - diff --git a/tests/posit/connect/oauth/test_integrations.py b/tests/posit/connect/oauth/test_integrations.py index fd4d5bf4..b98e2ac5 100644 --- a/tests/posit/connect/oauth/test_integrations.py +++ b/tests/posit/connect/oauth/test_integrations.py @@ -1,6 +1,6 @@ from unittest import mock -import pytest +import pytest import responses from responses import matchers @@ -59,9 +59,10 @@ def test_updated_time(self): def test_associations(self): assert isinstance(self.item.associations, IntegrationAssociations) + class TestIntegrationOAuthAssociationsError: def test(self): - fake_item = load_mock( + fake_item = load_mock( "v1/oauth/integrations/22644575-a27b-4118-ad06-e24459b05126.json" ) del fake_item["guid"] @@ -70,6 +71,7 @@ def test(self): with pytest.raises(ValueError): integration.associations + class TestIntegrationDelete: @responses.activate def test(self): diff --git a/tests/posit/connect/test_content.py b/tests/posit/connect/test_content.py index f4f27580..c920637e 100644 --- a/tests/posit/connect/test_content.py +++ b/tests/posit/connect/test_content.py @@ -5,7 +5,11 @@ from responses import matchers from posit.connect.client import Client -from posit.connect.content import ContentItem, ContentItemOwner +from posit.connect.content import ( + ContentItem, + ContentItemOAuth, + ContentItemOwner, +) from posit.connect.oauth.associations import ContentItemAssociations from posit.connect.permissions import Permissions @@ -179,23 +183,30 @@ def test_owner(self): def test_permissions(self): assert isinstance(self.item.permissions, Permissions) - + + def test_oauth(self): + assert isinstance(self.item.oauth, ContentItemOAuth) + def test_oauth_associations(self): - assert isinstance(self.item.oauth_associations, ContentItemAssociations) + assert isinstance( + self.item.oauth.associations, ContentItemAssociations + ) def test_tags(self): assert self.item.tags is None -class TestContentItemOAuthAssociationsError: + +class TestContentItemOAuthError: def test(self): - fake_item = load_mock( + fake_item = load_mock( "v1/content/f2f37341-e21d-3d80-c698-a935ad614066.json" ) del fake_item["guid"] content_item = ContentItem(mock.Mock(), **fake_item) with pytest.raises(ValueError): - content_item.oauth_associations + content_item.oauth + class TestContentItemGetContentOwner: @responses.activate From 20324d1be1ca6b07f9d1bcab7edfa8aa6b121324 Mon Sep 17 00:00:00 2001 From: zackverham <96081108+zackverham@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:52:22 -0400 Subject: [PATCH 3/8] union with Nonetype breaks older python versions --- .../posit/connect/oauth/test_associations.py | 2 +- src/posit/connect/oauth/associations.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/integration/tests/posit/connect/oauth/test_associations.py b/integration/tests/posit/connect/oauth/test_associations.py index d73a37e6..3c7586e4 100644 --- a/integration/tests/posit/connect/oauth/test_associations.py +++ b/integration/tests/posit/connect/oauth/test_associations.py @@ -103,6 +103,6 @@ def test_find_update_by_content(self): ) # unset content association - self.content.oauth.associations.update(None) + self.content.oauth.associations.delete() no_associations = self.content.oauth.associations.find() assert len(no_associations) == 0 diff --git a/src/posit/connect/oauth/associations.py b/src/posit/connect/oauth/associations.py index 0fef4ea5..cd6124ea 100644 --- a/src/posit/connect/oauth/associations.py +++ b/src/posit/connect/oauth/associations.py @@ -1,5 +1,6 @@ """OAuth association resources.""" +from os import walk from typing import List from ..resources import Resource, ResourceParameters, Resources @@ -65,12 +66,19 @@ def find(self) -> List[Association]: for result in response.json() ] - def update(self, integration_guid: None | str) -> None: + def delete(self) -> None: + """Delete integration associations.""" + data = [] + + path = ( + f"v1/content/{self.content_guid}/oauth/integrations/associations" + ) + url = self.params.url + path + self.params.session.put(url, json=data) + + def update(self, integration_guid: str) -> None: """Set integration associations.""" - if integration_guid is None: - data = [] - else: - data = [{"oauth_integration_guid": integration_guid}] + data = [{"oauth_integration_guid": integration_guid}] path = ( f"v1/content/{self.content_guid}/oauth/integrations/associations" From 2b875a0143c456a33d18405cc795c7dd370951be Mon Sep 17 00:00:00 2001 From: zackverham <96081108+zackverham@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:09:07 -0400 Subject: [PATCH 4/8] linting --- src/posit/connect/oauth/associations.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/posit/connect/oauth/associations.py b/src/posit/connect/oauth/associations.py index cd6124ea..f8e96b4d 100644 --- a/src/posit/connect/oauth/associations.py +++ b/src/posit/connect/oauth/associations.py @@ -1,6 +1,5 @@ """OAuth association resources.""" -from os import walk from typing import List from ..resources import Resource, ResourceParameters, Resources @@ -75,7 +74,7 @@ def delete(self) -> None: ) url = self.params.url + path self.params.session.put(url, json=data) - + def update(self, integration_guid: str) -> None: """Set integration associations.""" data = [{"oauth_integration_guid": integration_guid}] From 9f68e5b9a5f5ae9b9a4ce2c852e4f74ecf03b55d Mon Sep 17 00:00:00 2001 From: zackverham <96081108+zackverham@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:45:20 -0400 Subject: [PATCH 5/8] filling out tests for new delete function --- .../posit/connect/oauth/test_integrations.py | 6 ++-- .../posit/connect/oauth/test_associations.py | 31 ++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/integration/tests/posit/connect/oauth/test_integrations.py b/integration/tests/posit/connect/oauth/test_integrations.py index 9afc9c26..245b8d45 100644 --- a/integration/tests/posit/connect/oauth/test_integrations.py +++ b/integration/tests/posit/connect/oauth/test_integrations.py @@ -51,7 +51,7 @@ def teardown_class(cls): assert len(cls.client.oauth.integrations.find()) == 0 def test_get(self): - result = self.client.oauth.integrations.get(self.integration.guid) + result = self.client.oauth.integrations.get(self.integration["guid"]) assert result == self.integration def test_find(self): @@ -78,7 +78,7 @@ def test_create_update_delete(self): }, ) - created = self.client.oauth.integrations.get(integration.guid) + created = self.client.oauth.integrations.get(integration["guid"]) assert created == integration all_integrations = self.client.oauth.integrations.find() @@ -87,7 +87,7 @@ def test_create_update_delete(self): # update the new integration created.update(name="updated integration name") - updated = self.client.oauth.integrations.get(integration.guid) + updated = self.client.oauth.integrations.get(integration["guid"]) assert updated.name == "updated integration name" # delete the new integration diff --git a/tests/posit/connect/oauth/test_associations.py b/tests/posit/connect/oauth/test_associations.py index d8a5d699..f617fa30 100644 --- a/tests/posit/connect/oauth/test_associations.py +++ b/tests/posit/connect/oauth/test_associations.py @@ -107,7 +107,7 @@ def test(self): class TestContentAssociationsUpdate: @responses.activate - def test_single(self): + def test(self): guid = "f2f37341-e21d-3d80-c698-a935ad614066" # behavior @@ -132,3 +132,32 @@ def test_single(self): # assert assert mock_put.call_count == 1 assert mock_get_content.call_count == 1 + + +class TestContentAssociationsDelete: + @responses.activate + def test(self): + guid = "f2f37341-e21d-3d80-c698-a935ad614066" + + # behavior + mock_get_content = responses.get( + f"https://connect.example/__api__/v1/content/{guid}", + json=load_mock(f"v1/content/{guid}.json"), + ) + + mock_put = responses.put( + f"https://connect.example/__api__/v1/content/{guid}/oauth/integrations/associations", + json=[], + ) + + # setup + c = Client("https://connect.example", "12345") + + # invoke + c.content.get(guid).oauth.associations.delete() + + # assert + assert mock_put.call_count == 1 + assert mock_get_content.call_count == 1 + + From aae85b9b886f2f3164194c6b1cf84fd8710e1583 Mon Sep 17 00:00:00 2001 From: zackverham <96081108+zackverham@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:46:58 -0400 Subject: [PATCH 6/8] fixing linting --- src/posit/connect/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/posit/connect/client.py b/src/posit/connect/client.py index ddfb4116..fe950b4e 100644 --- a/src/posit/connect/client.py +++ b/src/posit/connect/client.py @@ -6,8 +6,6 @@ from requests import Response, Session -from posit.connect.resources import ResourceParameters - from . import hooks, me from .auth import Auth from .config import Config @@ -15,6 +13,7 @@ from .groups import Groups from .metrics import Metrics from .oauth import OAuth +from .resources import ResourceParameters from .tasks import Tasks from .users import User, Users From 03bf9ecda76e28ca2cdad077e41bb8120f29fa58 Mon Sep 17 00:00:00 2001 From: zackverham <96081108+zackverham@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:04:39 -0400 Subject: [PATCH 7/8] removing unneeded exception handling --- src/posit/connect/content.py | 2 -- src/posit/connect/oauth/integrations.py | 4 ---- tests/posit/connect/oauth/test_integrations.py | 12 ------------ tests/posit/connect/test_content.py | 12 ------------ 4 files changed, 30 deletions(-) diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py index 7044b61b..c6cde797 100644 --- a/src/posit/connect/content.py +++ b/src/posit/connect/content.py @@ -43,8 +43,6 @@ def __getitem__(self, key: Any) -> Any: @property def oauth(self) -> ContentItemOAuth: - if "guid" not in self: - raise ValueError("ContentItemOAuth requires content guid") return ContentItemOAuth(self.params, content_guid=self["guid"]) def delete(self) -> None: diff --git a/src/posit/connect/oauth/integrations.py b/src/posit/connect/oauth/integrations.py index 3ebc48f2..fd40dcce 100644 --- a/src/posit/connect/oauth/integrations.py +++ b/src/posit/connect/oauth/integrations.py @@ -12,10 +12,6 @@ class Integration(Resource): @property def associations(self) -> IntegrationAssociations: - if "guid" not in self: - raise ValueError( - "IntegrationAssociations requires integration guid" - ) return IntegrationAssociations( self.params, integration_guid=self["guid"] ) diff --git a/tests/posit/connect/oauth/test_integrations.py b/tests/posit/connect/oauth/test_integrations.py index b98e2ac5..3e1d2405 100644 --- a/tests/posit/connect/oauth/test_integrations.py +++ b/tests/posit/connect/oauth/test_integrations.py @@ -60,18 +60,6 @@ def test_associations(self): assert isinstance(self.item.associations, IntegrationAssociations) -class TestIntegrationOAuthAssociationsError: - def test(self): - fake_item = load_mock( - "v1/oauth/integrations/22644575-a27b-4118-ad06-e24459b05126.json" - ) - del fake_item["guid"] - integration = Integration(mock.Mock(), **fake_item) - - with pytest.raises(ValueError): - integration.associations - - class TestIntegrationDelete: @responses.activate def test(self): diff --git a/tests/posit/connect/test_content.py b/tests/posit/connect/test_content.py index c920637e..dfe632a3 100644 --- a/tests/posit/connect/test_content.py +++ b/tests/posit/connect/test_content.py @@ -196,18 +196,6 @@ def test_tags(self): assert self.item.tags is None -class TestContentItemOAuthError: - def test(self): - fake_item = load_mock( - "v1/content/f2f37341-e21d-3d80-c698-a935ad614066.json" - ) - del fake_item["guid"] - content_item = ContentItem(mock.Mock(), **fake_item) - - with pytest.raises(ValueError): - content_item.oauth - - class TestContentItemGetContentOwner: @responses.activate def test_owner(self): From 808ca7c3f82940ad38db98101886f4765ac710fe Mon Sep 17 00:00:00 2001 From: zackverham <96081108+zackverham@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:07:09 -0400 Subject: [PATCH 8/8] linting --- tests/posit/connect/oauth/test_associations.py | 2 -- tests/posit/connect/oauth/test_integrations.py | 1 - 2 files changed, 3 deletions(-) diff --git a/tests/posit/connect/oauth/test_associations.py b/tests/posit/connect/oauth/test_associations.py index f617fa30..27a2d889 100644 --- a/tests/posit/connect/oauth/test_associations.py +++ b/tests/posit/connect/oauth/test_associations.py @@ -159,5 +159,3 @@ def test(self): # assert assert mock_put.call_count == 1 assert mock_get_content.call_count == 1 - - diff --git a/tests/posit/connect/oauth/test_integrations.py b/tests/posit/connect/oauth/test_integrations.py index 3e1d2405..e7f2d470 100644 --- a/tests/posit/connect/oauth/test_integrations.py +++ b/tests/posit/connect/oauth/test_integrations.py @@ -1,6 +1,5 @@ from unittest import mock -import pytest import responses from responses import matchers