From 816a869887172a4a7735c8249e40aeca58257b44 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Wed, 16 Apr 2025 13:27:29 +0300 Subject: [PATCH 1/4] Add new factories for link addons --- addon_service/tests/_factories.py | 145 ++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/addon_service/tests/_factories.py b/addon_service/tests/_factories.py index 749a6cbf..d26b1409 100644 --- a/addon_service/tests/_factories.py +++ b/addon_service/tests/_factories.py @@ -187,3 +187,148 @@ def _create( *args, **kwargs, ) + + +### +# "Link" models + + +class ExternalLinkServiceFactory(DjangoModelFactory): + class Meta: + model = db.ExternalLinkService + + display_name = factory.Faker("word") + int_addon_imp = known_imps.get_imp_number( + known_imps.get_imp_by_name("LINK_DATAVERSE") + ) + supported_scopes = ["service.url/grant_all"] + + @classmethod + def _create( + cls, + model_class, + credentials_format=CredentialsFormats.PERSONAL_ACCESS_TOKEN, + service_type=ServiceTypes.PUBLIC, + supported_resource_types=None, + *args, + **kwargs, + ): + from addon_toolkit.interfaces.link import SupportedResourceTypes + + api_base_url = "" + if ServiceTypes.PUBLIC in service_type: + api_base_url = "https://api.example.url/v1" + + supported_resource_types = ( + supported_resource_types or SupportedResourceTypes.DATASET + ) + + return super()._create( + model_class=model_class, + int_credentials_format=credentials_format.value, + int_service_type=service_type.value, + int_supported_resource_types=supported_resource_types.value, + api_base_url=api_base_url, + *args, + **kwargs, + ) + + +class ExternalLinkOAuth2ServiceFactory(ExternalLinkServiceFactory): + credentials_format = CredentialsFormats.OAUTH2 + oauth2_client_config = factory.SubFactory(OAuth2ClientConfigFactory) + + +class ExternalLinkOAuth1ServiceFactory(ExternalLinkServiceFactory): + credentials_format = CredentialsFormats.OAUTH1A + oauth1_client_config = factory.SubFactory(OAuth1ClientConfigFactory) + + +class AuthorizedLinkAccountFactory(DjangoModelFactory): + class Meta: + model = db.AuthorizedLinkAccount + + display_name = factory.Faker("word") + authorized_capabilities = AddonCapabilities.ACCESS | AddonCapabilities.UPDATE + + @classmethod + def _create( + cls, + model_class, + account_owner=None, + external_service=None, + credentials=None, + credentials_format=CredentialsFormats.OAUTH2, + authorized_scopes=None, + *args, + **kwargs, + ): + from addon_service.tests._helpers import patch_encryption_key_derivation + + account = super()._create( + model_class=model_class, + external_service=external_service + or ExternalLinkOAuth2ServiceFactory(credentials_format=credentials_format), + account_owner=account_owner or UserReferenceFactory(), + *args, + **kwargs, + ) + if credentials_format is CredentialsFormats.OAUTH2: + account.initiate_oauth2_flow(authorized_scopes) + else: + with patch_encryption_key_derivation(): + from addon_service.tests.test_by_type.test_authorized_link_account import ( + MOCK_CREDENTIALS, + ) + + account.credentials = credentials or MOCK_CREDENTIALS.get( + credentials_format + ) + account.save() + return account + + +class ConfiguredLinkAddonFactory(DjangoModelFactory): + class Meta: + model = db.ConfiguredLinkAddon + + target_id = factory.Faker("uuid4") + connected_capabilities = AddonCapabilities.ACCESS + + @classmethod + def _create( + cls, + model_class, + authorized_resource=None, + external_link_service=None, + credentials_format=CredentialsFormats.OAUTH2, + base_account=None, + account_owner=None, + credentials=None, + resource_type=None, + *args, + **kwargs, + ): + from addon_toolkit.interfaces.link import SupportedResourceTypes + + authorized_resource = authorized_resource or ResourceReferenceFactory() + base_account = base_account or AuthorizedLinkAccountFactory( + external_service=external_link_service, + credentials_format=credentials_format, + account_owner=account_owner, + credentials=credentials, + ) + + resource_type = resource_type or SupportedResourceTypes.DATASET + + kwargs["int_resource_type"] = resource_type.value + + addon = super()._create( + model_class=model_class, + authorized_resource=authorized_resource, + base_account=base_account, + *args, + **kwargs, + ) + + return addon From 33eb2afc94eb4503c5985763d548c568f2d6c6d6 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Wed, 16 Apr 2025 13:30:39 +0300 Subject: [PATCH 2/4] Add tests for Dataverse link addon imp --- addon_imps/tests/link/test_dataverse.py | 304 ++++++++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 addon_imps/tests/link/test_dataverse.py diff --git a/addon_imps/tests/link/test_dataverse.py b/addon_imps/tests/link/test_dataverse.py new file mode 100644 index 00000000..dcb80e80 --- /dev/null +++ b/addon_imps/tests/link/test_dataverse.py @@ -0,0 +1,304 @@ +import unittest +from http import HTTPStatus +from unittest.mock import ( + AsyncMock, + patch, +) + +from django.core.exceptions import ValidationError + +from addon_imps.link.dataverse import DataverseLinkImp +from addon_toolkit.constrained_network.http import HttpRequestor +from addon_toolkit.interfaces.link import ( + ItemResult, + ItemSampleResult, + ItemType, +) + + +class TestDataverseLinkImp(unittest.IsolatedAsyncioTestCase): + def setUp(self): + self.base_url = "https://dataverse.org" + self.network = AsyncMock(spec_set=HttpRequestor) + self.imp = DataverseLinkImp(network=self.network) + + def _patch_get(self, return_value, status_code=200): + mock = self.network.GET.return_value.__aenter__.return_value + mock.json_content = AsyncMock(return_value=return_value) + mock.http_status = HTTPStatus(status_code) + return mock + + def _assert_get(self, url, query=None): + extra_params = {"query": query} if query else {} + self.network.GET.assert_called_once_with(url, **extra_params) + self.network.GET.return_value.__aenter__.assert_awaited_once_with() + self.network.GET.return_value.__aenter__.return_value.json_content.assert_awaited_once_with() + self.network.GET.return_value.__aexit__.assert_awaited_once_with( + None, None, None + ) + + async def test_build_url_for_id(self): + dataset_result = ItemResult( + item_id="dataset/doi:10.5072/FK2/ABCDEF", + item_name="Test Dataset", + item_type=ItemType.FOLDER, + ) + + with patch.object( + self.imp, "_fetch_dataset", new_callable=AsyncMock + ) as mock_fetch: + mock_fetch.return_value = dataset_result + + url = await self.imp.build_url_for_id("dataset/doi:10.5072/FK2/ABCDEF") + + mock_fetch.assert_awaited_once_with("doi:10.5072/FK2/ABCDEF") + + self.assertEqual(url, "dataset/doi:10.5072/FK2/ABCDEF") + + async def test_get_external_account_id_success(self): + self._patch_get({"data": {"id": "user123"}}) + + result = await self.imp.get_external_account_id({}) + + self._assert_get("api/v1/users/:me") + self.assertEqual(result, "user123") + + async def test_get_external_account_id_invalid_url(self): + self.network.GET.side_effect = ValueError( + "Relative URL may not alter the base URL" + ) + + with self.assertRaises(ValidationError) as context: + await self.imp.get_external_account_id({}) + + self.assertIn("Invalid host URL", str(context.exception)) + + async def test_list_root_items(self): + mock_response = { + "data": { + "items": [ + {"entity_id": "123", "name": "Dataverse 1"}, + {"entity_id": "456", "name": "Dataverse 2"}, + ], + "total_count": 2, + "pagination": { + "nextPageNumber": "2", + "hasNextPageNumber": True, + }, + } + } + self._patch_get(mock_response) + + result = await self.imp.list_root_items() + + expected_items = [ + ItemResult( + item_id="dataverse/123", + item_name="Dataverse 1", + item_type=ItemType.FOLDER, + ), + ItemResult( + item_id="dataverse/456", + item_name="Dataverse 2", + item_type=ItemType.FOLDER, + ), + ] + expected_result = ItemSampleResult( + items=expected_items, total_count=2, next_sample_cursor="2" + ) + + self.assertEqual(len(result.items), len(expected_result.items)) + for i, item in enumerate(result.items): + self.assertEqual(item.item_id, expected_result.items[i].item_id) + self.assertEqual(item.item_name, expected_result.items[i].item_name) + self.assertEqual(item.item_type, expected_result.items[i].item_type) + self.assertEqual(result.total_count, expected_result.total_count) + self.assertEqual(result.next_sample_cursor, expected_result.next_sample_cursor) + + query_params = [ + ["selected_page", ""], + *[("role_ids", role) for role in range(1, 9)], + ("dvobject_types", "Dataverse"), + *[ + ("published_states", state) + for state in [ + "Unpublished", + "Published", + "Draft", + "Deaccessioned", + "In+Review", + ] + ], + ] + self._assert_get("api/mydata/retrieve", query=query_params) + + async def test_list_root_items_with_page(self): + mock_response = { + "data": { + "items": [ + {"entity_id": "789", "name": "Dataverse 3"}, + ], + "total_count": 3, + "pagination": { + "nextPageNumber": "3", + "hasNextPageNumber": True, + }, + } + } + self._patch_get(mock_response) + + result = await self.imp.list_root_items(page_cursor="2") + + expected_items = [ + ItemResult( + item_id="dataverse/789", + item_name="Dataverse 3", + item_type=ItemType.FOLDER, + ), + ] + expected_result = ItemSampleResult( + items=expected_items, total_count=3, next_sample_cursor="3" + ) + + self.assertEqual(len(result.items), len(expected_result.items)) + self.assertEqual(result.items[0].item_id, expected_result.items[0].item_id) + self.assertEqual(result.next_sample_cursor, expected_result.next_sample_cursor) + + self.network.GET.assert_called_once() + call_args = self.network.GET.call_args[1] + self.assertEqual(call_args["query"][0][1], "2") + + async def test_list_root_items_empty_response(self): + self._patch_get({}) + + result = await self.imp.list_root_items() + + self.assertEqual(len(result.items), 0) + self.assertEqual(result.total_count, 0) + + async def test_get_item_info_empty(self): + result = await self.imp.get_item_info("") + + self.assertEqual(result.item_id, "") + self.assertEqual(result.item_name, "") + self.assertEqual(result.item_type, ItemType.FOLDER) + + async def test_get_item_info_dataverse(self): + dataverse_response = {"data": {"id": "123", "name": "Test Dataverse"}} + self._patch_get(dataverse_response) + + result = await self.imp.get_item_info("dataverse/123") + + expected_result = ItemResult( + item_id="dataverse/123", + item_name="Test Dataverse", + item_type=ItemType.FOLDER, + ) + + self.assertEqual(result.item_id, expected_result.item_id) + self.assertEqual(result.item_name, expected_result.item_name) + self.assertEqual(result.item_type, expected_result.item_type) + self._assert_get("api/dataverses/123") + + async def test_get_item_info_dataset(self): + dataset_response = { + "data": { + "latestVersion": { + "datasetPersistentId": "doi:10.5072/FK2/ABCDEF", + "metadataBlocks": { + "citation": { + "fields": [{"typeName": "title", "value": "Test Dataset"}] + } + }, + } + } + } + self._patch_get(dataset_response) + + result = await self.imp.get_item_info("dataset/doi:10.5072/FK2/ABCDEF") + + expected_result = ItemResult( + item_id="dataset/doi:10.5072/FK2/ABCDEF", + item_name="Test Dataset", + item_type=ItemType.FOLDER, + ) + + self.assertEqual(result.item_id, expected_result.item_id) + self.assertEqual(result.item_name, expected_result.item_name) + self.assertEqual(result.item_type, expected_result.item_type) + self._assert_get( + "api/datasets/:persistentId", + query={"persistentId": "doi:10.5072/FK2/ABCDEF"}, + ) + + async def test_get_item_info_invalid(self): + with self.assertRaises(ValueError): + await self.imp.get_item_info("invalid/123") + + async def test_list_child_items_empty(self): + self.imp.list_root_items = AsyncMock( + return_value=ItemSampleResult(items=[], total_count=0) + ) + + result = await self.imp.list_child_items("") + + self.imp.list_root_items.assert_awaited_once_with("") + self.assertEqual(len(result.items), 0) + self.assertEqual(result.total_count, 0) + + async def test_list_child_items_dataverse(self): + dataverse_contents = { + "data": [ + {"type": "dataverse", "id": "456", "title": "Sub Dataverse"}, + {"type": "dataset", "id": "789", "title": "Dataset"}, + ] + } + + self._patch_get(dataverse_contents) + + with patch.object( + self.imp, "_fetch_dataset", new_callable=AsyncMock + ) as mock_fetch_dataset: + mock_fetch_dataset.return_value = ItemResult( + item_id="dataset/789", + item_name="Dataset", + item_type=ItemType.FOLDER, + ) + + result = await self.imp.list_child_items("dataverse/123") + + self._assert_get("api/dataverses/123/contents") + + expected_items = [ + ItemResult( + item_id="dataset/789", + item_name="Dataset", + item_type=ItemType.FOLDER, + ), + ItemResult( + item_id="dataverse/456", + item_name="Sub Dataverse", + item_type=ItemType.FOLDER, + ), + ] + + self.assertEqual(len(result.items), len(expected_items)) + self.assertEqual(result.total_count, len(expected_items)) + + item_ids = [item.item_id for item in result.items] + expected_ids = [item.item_id for item in expected_items] + for expected_id in expected_ids: + self.assertIn(expected_id, item_ids) + + async def test_parse_invalid_dataset(self): + invalid_dataset = {"data": {}} + self._patch_get(invalid_dataset) + + with self.assertRaises(ValueError): + await self.imp.get_item_info("dataset/invalid") + + async def test_list_child_items_non_dataverse(self): + result = await self.imp.list_child_items("dataset/123") + + self.assertEqual(len(result.items), 0) + self.assertEqual(result.total_count, 0) From 50461c493d2a8d479bf0367cbd6b637bcafca1a4 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Wed, 16 Apr 2025 13:31:21 +0300 Subject: [PATCH 3/4] Add tests for authorized, configured and external service link models --- .../test_authorized_link_account.py | 396 ++++++++++++++++++ .../test_configured_link_addon.py | 328 +++++++++++++++ .../test_external_link_service.py | 258 ++++++++++++ 3 files changed, 982 insertions(+) create mode 100644 addon_service/tests/test_by_type/test_authorized_link_account.py create mode 100644 addon_service/tests/test_by_type/test_configured_link_addon.py create mode 100644 addon_service/tests/test_by_type/test_external_link_service.py diff --git a/addon_service/tests/test_by_type/test_authorized_link_account.py b/addon_service/tests/test_by_type/test_authorized_link_account.py new file mode 100644 index 00000000..2749c8be --- /dev/null +++ b/addon_service/tests/test_by_type/test_authorized_link_account.py @@ -0,0 +1,396 @@ +from http import HTTPStatus +from unittest.mock import ( + MagicMock, + patch, +) + +from django.conf import settings +from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APITestCase + +from addon_service import models as db +from addon_service.authorized_account.link.views import AuthorizedLinkAccountViewSet +from addon_service.common.credentials_formats import CredentialsFormats +from addon_service.common.service_types import ServiceTypes +from addon_service.tests import _factories +from addon_service.tests._helpers import ( + MockOSF, + get_test_request, + patch_encryption_key_derivation, +) +from addon_toolkit import AddonCapabilities +from addon_toolkit.credentials import ( + AccessKeySecretKeyCredentials, + AccessTokenCredentials, + UsernamePasswordCredentials, +) + + +MOCK_CREDENTIALS = { + CredentialsFormats.OAUTH2: None, + CredentialsFormats.PERSONAL_ACCESS_TOKEN: AccessTokenCredentials( + access_token="token" + ), + CredentialsFormats.ACCESS_KEY_SECRET_KEY: AccessKeySecretKeyCredentials( + access_key="access", + secret_key="secret", + ), + CredentialsFormats.USERNAME_PASSWORD: UsernamePasswordCredentials( + username="me", + password="unsafe", + ), + CredentialsFormats.DATAVERSE_API_TOKEN: AccessTokenCredentials( + access_token="token" + ), +} + + +def _make_post_payload( + *, + external_service, + capabilities=None, + credentials=None, + api_root="", + display_name="MY ACCOUNT MINE", + initiate_oauth=True, +): + capabilities = capabilities or [AddonCapabilities.ACCESS.name] + payload = { + "data": { + "type": "authorized-link-accounts", + "attributes": { + "display_name": display_name, + "authorized_capabilities": capabilities, + "api_base_url": api_root, + "initiate_oauth": initiate_oauth, + }, + "relationships": { + "external_link_service": { + "data": { + "type": "external-link-services", + "id": str(external_service.id), + } + }, + }, + } + } + credentials = credentials or MOCK_CREDENTIALS[external_service.credentials_format] + if credentials: + from addon_service.common import json_arguments + + payload["data"]["attributes"]["credentials"] = ( + json_arguments.json_for_dataclass(credentials) + ) + return payload + + +def mock_get_link_addon_instance(*args, **kwargs): + mock_instance = MagicMock() + mock_instance.build_url_for_id = MagicMock( + return_value="https://example.com/dataset/123" + ) + mock_instance.get_external_account_id = MagicMock(return_value="test-account-id") + return mock_instance + + +class TestAuthorizedLinkAccountAPI(APITestCase): + @classmethod + def setUpTestData(cls): + cls._ala = _factories.AuthorizedLinkAccountFactory() + cls._user = cls._ala.account_owner + + def setUp(self): + super().setUp() + self.client.cookies[settings.USER_REFERENCE_COOKIE] = self._user.user_uri + self._mock_osf = MockOSF() + self._mock_osf.configure_assumed_caller(self._user.user_uri) + self.enterContext(self._mock_osf.mocking()) + + self.instantiation_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance", + mock_get_link_addon_instance, + ) + self.instantiation_blocking_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance__blocking", + mock_get_link_addon_instance, + ) + self.instantiation_patcher.start() + self.instantiation_blocking_patcher.start() + self.addCleanup(self.instantiation_patcher.stop) + self.addCleanup(self.instantiation_blocking_patcher.stop) + + @property + def _detail_path(self): + return reverse( + "authorized-link-accounts-detail", + kwargs={"pk": self._ala.pk}, + ) + + @property + def _list_path(self): + return reverse("authorized-link-accounts-list") + + def _related_path(self, related_field): + return reverse( + "authorized-link-accounts-related", + kwargs={ + "pk": self._ala.pk, + "related_field": related_field, + }, + ) + + def test_get_detail(self): + _resp = self.client.get(self._detail_path) + self.assertEqual(_resp.status_code, HTTPStatus.OK) + self.assertEqual(_resp.data["display_name"], self._ala.display_name) + + def test_post(self): + external_service = _factories.ExternalLinkOAuth2ServiceFactory() + self.assertFalse(external_service.authorized_link_accounts.exists()) + + _resp = self.client.post( + reverse("authorized-link-accounts-list"), + _make_post_payload( + external_service=external_service, display_name="test link account" + ), + format="vnd.api+json", + ) + self.assertEqual(_resp.status_code, HTTPStatus.CREATED) + + _from_db = external_service.authorized_link_accounts.get(id=_resp.data["id"]) + self.assertEqual(_from_db.display_name, "test link account") + + def test_methods_not_allowed(self): + _methods_not_allowed = { + self._detail_path: {"put"}, + self._list_path: {"put"}, + } + for _path, _methods in _methods_not_allowed.items(): + for _method in _methods: + with self.subTest(path=_path, method=_method): + _client_method = getattr(self.client, _method) + _resp = _client_method(_path) + self.assertEqual(_resp.status_code, HTTPStatus.METHOD_NOT_ALLOWED) + + +class TestAuthorizedLinkAccountModel(TestCase): + @classmethod + def setUpTestData(cls): + cls._user = _factories.UserReferenceFactory() + cls._account = _factories.AuthorizedLinkAccountFactory( + account_owner=cls._user, + ) + + def setUp(self): + self._mock_osf = MockOSF() + self.enterContext(self._mock_osf.mocking()) + + self.instantiation_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance", + mock_get_link_addon_instance, + ) + self.instantiation_blocking_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance__blocking", + mock_get_link_addon_instance, + ) + self.instantiation_patcher.start() + self.instantiation_blocking_patcher.start() + self.addCleanup(self.instantiation_patcher.stop) + self.addCleanup(self.instantiation_blocking_patcher.stop) + + def test_can_load(self): + _account_from_db = db.AuthorizedLinkAccount.objects.get(id=self._account.id) + self.assertEqual(self._account.display_name, _account_from_db.display_name) + + def test_configured_link_addons__empty(self): + self.assertEqual( + list(self._account.configured_link_addons.all()), + [], + ) + + def test_configured_link_addons__several(self): + _addons = set( + _factories.ConfiguredLinkAddonFactory.create_batch( + size=3, + base_account=self._account, + ) + ) + self.assertEqual( + set(self._account.configured_link_addons.all()), + _addons, + ) + + def test_set_credentials__create(self): + for creds_format in [ + CredentialsFormats.PERSONAL_ACCESS_TOKEN, + CredentialsFormats.ACCESS_KEY_SECRET_KEY, + CredentialsFormats.USERNAME_PASSWORD, + ]: + account = _factories.AuthorizedLinkAccountFactory( + credentials_format=creds_format + ) + with self.subTest(creds_format=creds_format): + with patch_encryption_key_derivation(): + account.credentials = MOCK_CREDENTIALS[creds_format] + account.save() + + refreshed = db.AuthorizedLinkAccount.objects.get(id=account.id) + + self.assertTrue(refreshed.credentials_available) + + with patch_encryption_key_derivation(): + self.assertEqual( + refreshed.credentials, + MOCK_CREDENTIALS[creds_format], + ) + + def test_capabilities(self): + new_capabilities = AddonCapabilities.ACCESS | AddonCapabilities.UPDATE + self._account.authorized_capabilities = new_capabilities + self._account.save() + + refreshed = db.AuthorizedLinkAccount.objects.get(id=self._account.id) + self.assertEqual(refreshed.authorized_capabilities, new_capabilities) + + +class TestAuthorizedLinkAccountViewSet(TestCase): + @classmethod + def setUpTestData(cls): + cls._user = _factories.UserReferenceFactory() + cls._account = _factories.AuthorizedLinkAccountFactory( + account_owner=cls._user, + ) + cls._view = AuthorizedLinkAccountViewSet.as_view({"get": "retrieve"}) + + cls._addons = _factories.ConfiguredLinkAddonFactory.create_batch( + size=2, + base_account=cls._account, + ) + + def setUp(self): + self._mock_osf = MockOSF() + self._mock_osf.configure_assumed_caller(self._user.user_uri) + self.enterContext(self._mock_osf.mocking()) + + self.instantiation_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance", + mock_get_link_addon_instance, + ) + self.instantiation_blocking_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance__blocking", + mock_get_link_addon_instance, + ) + self.instantiation_patcher.start() + self.instantiation_blocking_patcher.start() + self.addCleanup(self.instantiation_patcher.stop) + self.addCleanup(self.instantiation_blocking_patcher.stop) + + def test_get(self): + request = get_test_request(user=self._user) + request.session = {"user_reference_uri": self._user.user_uri} + request.COOKIES = {settings.USER_REFERENCE_COOKIE: self._user.user_uri} + _resp = self._view( + request, + pk=self._account.pk, + ) + self.assertEqual(_resp.status_code, HTTPStatus.OK) + + with self.subTest("Confirm expected keys"): + expected_fields = { + "authorized_capabilities", + "authorized_operation_names", + "credentials_available", + "display_name", + "api_base_url", + } + for field in expected_fields: + self.assertIn(field, _resp.data.keys()) + + with self.subTest("Confirm expected relationships"): + relationship_fields = { + key for key, value in _resp.data.items() if isinstance(value, dict) + } + for relation in ["account_owner", "external_link_service"]: + self.assertIn(relation, relationship_fields) + + def test_owner_access(self): + request = get_test_request(user=self._user) + request.session = {"user_reference_uri": self._user.user_uri} + request.COOKIES = {settings.USER_REFERENCE_COOKIE: self._user.user_uri} + _resp = self._view( + request, + pk=self._account.pk, + ) + self.assertEqual(_resp.status_code, HTTPStatus.OK) + + def test_wrong_user(self): + _another_user = _factories.UserReferenceFactory() + self._mock_osf.configure_assumed_caller(_another_user.user_uri) + request = get_test_request(user=_another_user) + request.session = {"user_reference_uri": _another_user.user_uri} + request.COOKIES = {settings.USER_REFERENCE_COOKIE: _another_user.user_uri} + _resp = self._view( + request, + pk=self._account.pk, + ) + self.assertEqual(_resp.status_code, HTTPStatus.FORBIDDEN) + + +class TestCreateAuthorizedLinkAccount(APITestCase): + @classmethod + def setUpTestData(cls): + cls._user = _factories.UserReferenceFactory() + cls._external_service = _factories.ExternalLinkOAuth2ServiceFactory() + + def setUp(self): + self.client.cookies[settings.USER_REFERENCE_COOKIE] = self._user.user_uri + + self._mock_osf = MockOSF() + self._mock_osf.configure_assumed_caller(self._user.user_uri) + self.enterContext(self._mock_osf.mocking()) + + self.instantiation_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance", + mock_get_link_addon_instance, + ) + self.instantiation_blocking_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance__blocking", + mock_get_link_addon_instance, + ) + + self.instantiation_patcher.start() + self.instantiation_blocking_patcher.start() + + self.addCleanup(self.instantiation_patcher.stop) + self.addCleanup(self.instantiation_blocking_patcher.stop) + + def test_create_account(self): + external_service = _factories.ExternalLinkOAuth2ServiceFactory( + service_type=ServiceTypes.PUBLIC | ServiceTypes.HOSTED, + ) + self.assertFalse(external_service.authorized_link_accounts.exists()) + + capabilities = ["ACCESS"] + + _resp = self.client.post( + reverse("authorized-link-accounts-list"), + _make_post_payload( + external_service=external_service, + display_name="Test Link Account", + api_root="https://api.example.com", + capabilities=capabilities, + initiate_oauth=False, + ), + format="vnd.api+json", + ) + + self.assertEqual(_resp.status_code, HTTPStatus.CREATED) + + self.assertEqual(_resp.data["display_name"], "Test Link Account") + self.assertIn("ACCESS", _resp.data["authorized_capabilities"]) + + account = db.AuthorizedLinkAccount.objects.get(id=_resp.data["id"]) + self.assertEqual(account.display_name, "Test Link Account") + self.assertEqual(account.api_base_url, "https://api.example.com") + self.assertEqual(account.external_service.id, external_service.id) diff --git a/addon_service/tests/test_by_type/test_configured_link_addon.py b/addon_service/tests/test_by_type/test_configured_link_addon.py new file mode 100644 index 00000000..3e9902ca --- /dev/null +++ b/addon_service/tests/test_by_type/test_configured_link_addon.py @@ -0,0 +1,328 @@ +from http import HTTPStatus +from unittest.mock import ( + MagicMock, + patch, +) + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APITestCase + +from addon_service import models as db +from addon_service.configured_addon.link.models import is_supported_resource_type +from addon_service.configured_addon.link.views import ConfiguredLinkAddonViewSet +from addon_service.tests import _factories +from addon_service.tests._helpers import ( + MockOSF, + get_test_request, +) +from addon_toolkit import AddonCapabilities +from addon_toolkit.interfaces.link import SupportedResourceTypes + + +def mock_target_url(self): + return f"https://example.com/dataset/{self.target_id}" if self.target_id else None + + +def mock_get_link_addon_instance(*args, **kwargs): + mock_instance = MagicMock() + mock_instance.build_url_for_id = MagicMock( + return_value="https://example.com/dataset/123" + ) + mock_instance.get_external_account_id = MagicMock(return_value="test-account-id") + return mock_instance + + +class TestConfiguredLinkAddonAPI(APITestCase): + @classmethod + def setUpTestData(cls): + cls._user = _factories.UserReferenceFactory() + cls._resource = _factories.ResourceReferenceFactory() + cls._authorized_account = _factories.AuthorizedLinkAccountFactory( + account_owner=cls._user + ) + cls._addon = _factories.ConfiguredLinkAddonFactory( + authorized_resource=cls._resource, + base_account=cls._authorized_account, + ) + + def setUp(self): + super().setUp() + self.client.cookies[settings.USER_REFERENCE_COOKIE] = self._user.user_uri + self._mock_osf = MockOSF() + self._mock_osf.configure_user_role( + self._user.user_uri, self._resource.resource_uri, "admin" + ) + self._mock_osf.configure_assumed_caller(self._user.user_uri) + self.enterContext(self._mock_osf.mocking()) + + self.target_url_patcher = patch( + "addon_service.configured_addon.link.models.ConfiguredLinkAddon.target_url", + mock_target_url, + ) + self.target_url_patcher.start() + self.addCleanup(self.target_url_patcher.stop) + + @property + def _detail_path(self): + return reverse("configured-link-addons-detail", kwargs={"pk": self._addon.pk}) + + @property + def _list_path(self): + return reverse("configured-link-addons-list") + + def test_get(self): + _resp = self.client.get(self._detail_path) + self.assertEqual(_resp.status_code, HTTPStatus.OK) + self.assertEqual(_resp.data["target_id"], self._addon.target_id) + self.assertEqual(_resp.data["resource_type"], self._addon.resource_type) + + def test_methods_not_allowed(self): + _methods_not_allowed = { + self._list_path: {"patch", "put"}, + } + for _path, _methods in _methods_not_allowed.items(): + for _method in _methods: + with self.subTest(path=_path, method=_method): + _client_method = getattr(self.client, _method) + _resp = _client_method(_path) + self.assertEqual(_resp.status_code, HTTPStatus.METHOD_NOT_ALLOWED) + + +class TestConfiguredLinkAddonModel(TestCase): + @classmethod + def setUpTestData(cls): + cls._addon = _factories.ConfiguredLinkAddonFactory() + + def setUp(self): + self._mock_osf = MockOSF() + self.enterContext(self._mock_osf.mocking()) + + self.target_url_patcher = patch( + "addon_service.configured_addon.link.models.ConfiguredLinkAddon.target_url", + mock_target_url, + ) + self.target_url_patcher.start() + self.addCleanup(self.target_url_patcher.stop) + + def test_can_load(self): + _addon_from_db = db.ConfiguredLinkAddon.objects.get(id=self._addon.id) + self.assertEqual(self._addon.target_id, _addon_from_db.target_id) + self.assertEqual(self._addon.resource_type, _addon_from_db.resource_type) + + def test_resource_type_property(self): + self._addon.resource_type = SupportedResourceTypes.BOOK + self._addon.save() + + refreshed = db.ConfiguredLinkAddon.objects.get(id=self._addon.id) + self.assertEqual(refreshed.resource_type, "BOOK") + + self._addon.resource_type = SupportedResourceTypes.DATASET + self._addon.save() + + refreshed = db.ConfiguredLinkAddon.objects.get(id=self._addon.id) + self.assertEqual(refreshed.resource_type, "DATASET") + + def test_validator_valid_types(self): + try: + is_supported_resource_type(SupportedResourceTypes.DATASET.value) + is_supported_resource_type(SupportedResourceTypes.JOURNAL.value) + is_supported_resource_type(SupportedResourceTypes.SOFTWARE.value) + except ValidationError: + self.fail("Validator raised ValidationError unexpectedly on valid types") + + def test_validator_invalid_type(self): + with self.assertRaises(ValidationError): + is_supported_resource_type(-999) + + combined = ( + SupportedResourceTypes.DATASET.value | SupportedResourceTypes.JOURNAL.value + ) + with self.assertRaises(ValidationError): + is_supported_resource_type(combined) + + def test_validation_on_save(self): + self._addon.int_resource_type = ( + SupportedResourceTypes.DATASET.value | SupportedResourceTypes.JOURNAL.value + ) + with self.assertRaises(ValidationError): + self._addon.clean_fields() + + self._addon.int_resource_type = -999 + with self.assertRaises(ValidationError): + self._addon.clean_fields() + + def test_target_url(self): + addon = _factories.ConfiguredLinkAddonFactory() + addon.target_id = "" + self.assertIsNone(addon.target_url()) + + addon.target_id = "test-id" + self.assertEqual(addon.target_url(), "https://example.com/dataset/test-id") + + +class TestConfiguredLinkAddonViewSet(TestCase): + @classmethod + def setUpTestData(cls): + cls._user = _factories.UserReferenceFactory() + cls._resource = _factories.ResourceReferenceFactory() + cls._authorized_account = _factories.AuthorizedLinkAccountFactory( + account_owner=cls._user + ) + cls._addon = _factories.ConfiguredLinkAddonFactory( + authorized_resource=cls._resource, + base_account=cls._authorized_account, + ) + cls._view = ConfiguredLinkAddonViewSet.as_view({"get": "retrieve"}) + + def setUp(self): + self._mock_osf = MockOSF() + self._mock_osf.configure_user_role( + self._user.user_uri, self._resource.resource_uri, "admin" + ) + self._mock_osf.configure_assumed_caller(self._user.user_uri) + self.enterContext(self._mock_osf.mocking()) + + self.target_url_patcher = patch( + "addon_service.configured_addon.link.models.ConfiguredLinkAddon.target_url", + mock_target_url, + ) + self.target_url_patcher.start() + self.addCleanup(self.target_url_patcher.stop) + + def test_get(self): + request = get_test_request(user=self._user) + request.session = {"user_reference_uri": self._user.user_uri} + request.COOKIES = {settings.USER_REFERENCE_COOKIE: self._user.user_uri} + + _resp = self._view( + request, + pk=self._addon.pk, + ) + self.assertEqual(_resp.status_code, HTTPStatus.OK) + + with self.subTest("Confirm expected attributes"): + self.assertEqual(_resp.data["target_id"], self._addon.target_id) + self.assertEqual(_resp.data["resource_type"], self._addon.resource_type) + self.assertIn("connected_operation_names", _resp.data) + + with self.subTest("Confirm expected relationships"): + relationship_fields = { + key for key, value in _resp.data.items() if isinstance(value, dict) + } + self.assertIn("base_account", relationship_fields) + self.assertIn("authorized_resource", relationship_fields) + + def test_owner_access(self): + request = get_test_request(user=self._user) + request.session = {"user_reference_uri": self._user.user_uri} + request.COOKIES = {settings.USER_REFERENCE_COOKIE: self._user.user_uri} + + _resp = self._view( + request, + pk=self._addon.pk, + ) + self.assertEqual(_resp.status_code, HTTPStatus.OK) + + def test_wrong_user(self): + _another_user = _factories.UserReferenceFactory() + self._mock_osf.configure_assumed_caller(_another_user.user_uri) + + request = get_test_request(user=_another_user) + request.session = {"user_reference_uri": _another_user.user_uri} + request.COOKIES = {settings.USER_REFERENCE_COOKIE: _another_user.user_uri} + + _resp = self._view( + request, + pk=self._addon.pk, + ) + self.assertEqual(_resp.status_code, HTTPStatus.FORBIDDEN) + + +class TestCreateConfiguredLinkAddon(APITestCase): + @classmethod + def setUpTestData(cls): + cls._user = _factories.UserReferenceFactory() + cls._resource = _factories.ResourceReferenceFactory() + cls._authorized_account = _factories.AuthorizedLinkAccountFactory( + account_owner=cls._user, + authorized_capabilities=AddonCapabilities.ACCESS, + ) + + def setUp(self): + self._mock_osf = MockOSF() + self._mock_osf.configure_user_role( + self._user.user_uri, self._resource.resource_uri, "admin" + ) + self._mock_osf.configure_assumed_caller(self._user.user_uri) + self.enterContext(self._mock_osf.mocking()) + + self.target_url_patcher = patch( + "addon_service.configured_addon.link.models.ConfiguredLinkAddon.target_url", + mock_target_url, + ) + self.instance_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance", + mock_get_link_addon_instance, + ) + self.instance_blocking_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance__blocking", + mock_get_link_addon_instance, + ) + + self.target_url_patcher.start() + self.instance_patcher.start() + self.instance_blocking_patcher.start() + + self.addCleanup(self.target_url_patcher.stop) + self.addCleanup(self.instance_patcher.stop) + self.addCleanup(self.instance_blocking_patcher.stop) + + def test_create_addon(self): + self.client.cookies[settings.USER_REFERENCE_COOKIE] = self._user.user_uri + self._mock_osf.configure_user_role( + self._user.user_uri, self._resource.resource_uri, "admin" + ) + self._mock_osf.configure_assumed_caller(self._user.user_uri) + + request_data = { + "data": { + "type": "configured-link-addons", + "attributes": { + "target_id": "some-target-id", + "resource_type": "DATASET", + "connected_capabilities": ["ACCESS"], + "authorized_resource_uri": self._resource.resource_uri, + }, + "relationships": { + "base_account": { + "data": { + "type": "authorized-link-accounts", + "id": str(self._authorized_account.id), + } + }, + "authorized_resource": { + "data": { + "type": "resource-references", + "id": str(self._resource.id), + } + }, + }, + } + } + + _resp = self.client.post( + reverse("configured-link-addons-list"), + request_data, + format="vnd.api+json", + ) + + self.assertEqual(_resp.status_code, HTTPStatus.CREATED) + + self.assertEqual(_resp.data["resource_type"], "DATASET") + + addon = db.ConfiguredLinkAddon.objects.get(id=_resp.data["id"]) + self.assertEqual(addon.target_id, "some-target-id") + self.assertEqual(addon.resource_type, "DATASET") diff --git a/addon_service/tests/test_by_type/test_external_link_service.py b/addon_service/tests/test_by_type/test_external_link_service.py new file mode 100644 index 00000000..38f4b2c4 --- /dev/null +++ b/addon_service/tests/test_by_type/test_external_link_service.py @@ -0,0 +1,258 @@ +from http import HTTPStatus +from unittest.mock import ( + MagicMock, + patch, +) + +from django.core.exceptions import ValidationError +from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APITestCase + +from addon_service import models as db +from addon_service.common.credentials_formats import CredentialsFormats +from addon_service.external_service.link.views import ExternalLinkServiceViewSet +from addon_service.tests import _factories +from addon_service.tests._helpers import ( + MockOSF, + get_test_request, +) +from addon_toolkit.interfaces.link import SupportedResourceTypes + + +def mock_get_link_addon_instance(*args, **kwargs): + mock_instance = MagicMock() + mock_instance.build_url_for_id = MagicMock( + return_value="https://example.com/dataset/123" + ) + mock_instance.get_external_account_id = MagicMock(return_value="test-account-id") + return mock_instance + + +class TestExternalLinkServiceAPI(APITestCase): + @classmethod + def setUpTestData(cls): + cls._els = _factories.ExternalLinkOAuth2ServiceFactory() + + def setUp(self): + super().setUp() + self._mock_osf = MockOSF() + self.enterContext(self._mock_osf.mocking()) + + self.instantiation_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance", + mock_get_link_addon_instance, + ) + self.instantiation_blocking_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance__blocking", + mock_get_link_addon_instance, + ) + self.instantiation_patcher.start() + self.instantiation_blocking_patcher.start() + self.addCleanup(self.instantiation_patcher.stop) + self.addCleanup(self.instantiation_blocking_patcher.stop) + + @property + def _detail_path(self): + return reverse("external-link-services-detail", kwargs={"pk": self._els.pk}) + + @property + def _list_path(self): + return reverse("external-link-services-list") + + @property + def _related_authorized_link_accounts_path(self): + return reverse( + "external-link-services-related", + kwargs={ + "pk": self._els.pk, + "related_field": "authorized_link_accounts", + }, + ) + + def test_get(self): + _resp = self.client.get(self._detail_path) + self.assertEqual(_resp.status_code, HTTPStatus.OK) + self.assertEqual(_resp.data["auth_uri"], self._els.auth_uri) + + def test_methods_not_allowed(self): + _methods_not_allowed = { + self._detail_path: {"post"}, + self._list_path: {"patch", "put", "post"}, + self._related_authorized_link_accounts_path: {"patch", "put", "post"}, + } + for _path, _methods in _methods_not_allowed.items(): + for _method in _methods: + with self.subTest(path=_path, method=_method): + _client_method = getattr(self.client, _method) + _resp = _client_method(_path) + self.assertEqual(_resp.status_code, HTTPStatus.METHOD_NOT_ALLOWED) + + +class TestExternalLinkServiceModel(TestCase): + @classmethod + def setUpTestData(cls): + cls._els = _factories.ExternalLinkOAuth2ServiceFactory() + + def setUp(self): + self.instantiation_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance", + mock_get_link_addon_instance, + ) + self.instantiation_blocking_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance__blocking", + mock_get_link_addon_instance, + ) + self.instantiation_patcher.start() + self.instantiation_blocking_patcher.start() + self.addCleanup(self.instantiation_patcher.stop) + self.addCleanup(self.instantiation_blocking_patcher.stop) + + def test_can_load(self): + _resource_from_db = db.ExternalLinkService.objects.get(id=self._els.id) + self.assertEqual(self._els.auth_uri, _resource_from_db.auth_uri) + + def test_authorized_link_accounts__empty(self): + self.assertEqual( + list(self._els.authorized_link_accounts.all()), + [], + ) + + def test_authorized_link_accounts__several(self): + _accounts = set( + _factories.AuthorizedLinkAccountFactory.create_batch( + size=3, + external_service=self._els, + ) + ) + self.assertEqual( + set(self._els.authorized_link_accounts.all()), + _accounts, + ) + + def test_supported_resource_types_property(self): + self._els.supported_resource_types = SupportedResourceTypes.DATASET + self._els.save() + + refreshed = db.ExternalLinkService.objects.get(id=self._els.id) + self.assertEqual( + refreshed.supported_resource_types, SupportedResourceTypes.DATASET + ) + + multi_type = SupportedResourceTypes.DATASET | SupportedResourceTypes.PROJECT + self._els.supported_resource_types = multi_type + self._els.save() + + refreshed = db.ExternalLinkService.objects.get(id=self._els.id) + self.assertEqual(refreshed.supported_resource_types, multi_type) + + def test_validation__invalid_format(self): + service = _factories.ExternalLinkOAuth2ServiceFactory() + service.int_credentials_format = -1 + with self.assertRaises(ValidationError): + service.save() + + def test_validation__unsupported_format(self): + service = _factories.ExternalLinkOAuth2ServiceFactory() + service.int_credentials_format = CredentialsFormats.UNSPECIFIED.value + with self.assertRaises(ValidationError): + service.save() + + def test_validation__oauth_creds_require_client_config(self): + service = _factories.ExternalLinkOAuth2ServiceFactory( + credentials_format=CredentialsFormats.OAUTH2 + ) + service.oauth2_client_config = None + with self.assertRaises(ValidationError): + service.save() + + +class TestExternalLinkServiceViewSet(APITestCase): + @classmethod + def setUpTestData(cls): + cls._els = _factories.ExternalLinkOAuth2ServiceFactory() + cls._view = ExternalLinkServiceViewSet.as_view({"get": "retrieve"}) + cls._user = _factories.UserReferenceFactory() + + def setUp(self): + self._mock_osf = MockOSF() + self._mock_osf.configure_assumed_caller(self._user.user_uri) + self.enterContext(self._mock_osf.mocking()) + + self.instantiation_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance", + mock_get_link_addon_instance, + ) + self.instantiation_blocking_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance__blocking", + mock_get_link_addon_instance, + ) + self.instantiation_patcher.start() + self.instantiation_blocking_patcher.start() + self.addCleanup(self.instantiation_patcher.stop) + self.addCleanup(self.instantiation_blocking_patcher.stop) + + def test_get(self): + _resp = self._view( + get_test_request(), + pk=self._els.pk, + ) + self.assertEqual(_resp.status_code, HTTPStatus.OK) + + with self.subTest("Confirm expected keys"): + self.assertIn("supported_resource_types", _resp.data.keys()) + self.assertIn("display_name", _resp.data.keys()) + self.assertIn("credentials_format", _resp.data.keys()) + + with self.subTest("Confirm expected relationships"): + relationship_fields = { + key for key, value in _resp.data.items() if isinstance(value, dict) + } + self.assertIn("addon_imp", relationship_fields) + + def test_unauthorized(self): + _anon_resp = self._view(get_test_request(), pk=self._els.pk) + self.assertEqual(_anon_resp.status_code, HTTPStatus.OK) + + def test_wrong_user(self): + _another_user = _factories.UserReferenceFactory() + _resp = self._view( + get_test_request(user=_another_user), + pk=self._els.pk, + ) + self.assertEqual(_resp.status_code, HTTPStatus.OK) + + +class TestExternalLinkServiceRelatedView(APITestCase): + @classmethod + def setUpTestData(cls): + cls._els = _factories.ExternalLinkOAuth2ServiceFactory() + cls._related_view = ExternalLinkServiceViewSet.as_view( + {"get": "retrieve_related"}, + ) + + def setUp(self): + self._mock_osf = MockOSF() + self.enterContext(self._mock_osf.mocking()) + + self.instantiation_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance", + mock_get_link_addon_instance, + ) + self.instantiation_blocking_patcher = patch( + "addon_service.addon_imp.instantiation.get_link_addon_instance__blocking", + mock_get_link_addon_instance, + ) + self.instantiation_patcher.start() + self.instantiation_blocking_patcher.start() + self.addCleanup(self.instantiation_patcher.stop) + self.addCleanup(self.instantiation_blocking_patcher.stop) + + def test_get_related(self): + _resp = self._related_view( + get_test_request(), + pk=self._els.pk, + related_field="addon_imp", + ) + self.assertEqual(_resp.status_code, HTTPStatus.OK) + self.assertEqual(_resp.data["name"], self._els.addon_imp.name) From c7730b689e752527edc3c184810bb0006a87d765 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Tue, 22 Apr 2025 15:29:58 +0300 Subject: [PATCH 4/4] Update tests for dataverse link imp --- addon_imps/tests/link/test_dataverse.py | 34 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/addon_imps/tests/link/test_dataverse.py b/addon_imps/tests/link/test_dataverse.py index dcb80e80..fde430be 100644 --- a/addon_imps/tests/link/test_dataverse.py +++ b/addon_imps/tests/link/test_dataverse.py @@ -240,7 +240,7 @@ async def test_list_child_items_empty(self): return_value=ItemSampleResult(items=[], total_count=0) ) - result = await self.imp.list_child_items("") + result = await self.imp.list_child_items("", item_type=None) self.imp.list_root_items.assert_awaited_once_with("") self.assertEqual(len(result.items), 0) @@ -265,7 +265,7 @@ async def test_list_child_items_dataverse(self): item_type=ItemType.FOLDER, ) - result = await self.imp.list_child_items("dataverse/123") + result = await self.imp.list_child_items("dataverse/123", item_type=None) self._assert_get("api/dataverses/123/contents") @@ -298,7 +298,35 @@ async def test_parse_invalid_dataset(self): await self.imp.get_item_info("dataset/invalid") async def test_list_child_items_non_dataverse(self): - result = await self.imp.list_child_items("dataset/123") + result = await self.imp.list_child_items("dataset/123", item_type=None) self.assertEqual(len(result.items), 0) self.assertEqual(result.total_count, 0) + + async def test_list_child_items_with_item_type(self): + dataverse_contents = { + "data": [ + {"type": "dataverse", "id": "456", "title": "Sub Dataverse"}, + {"type": "dataset", "id": "789", "title": "Dataset"}, + ] + } + + self._patch_get(dataverse_contents) + + with patch.object( + self.imp, "_fetch_dataset", new_callable=AsyncMock + ) as mock_fetch_dataset: + mock_fetch_dataset.return_value = ItemResult( + item_id="dataset/789", + item_name="Dataset", + item_type=ItemType.FOLDER, + ) + + result = await self.imp.list_child_items( + "dataverse/123", item_type=ItemType.FOLDER + ) + + self._assert_get("api/dataverses/123/contents") + + for item in result.items: + self.assertEqual(item.item_type, ItemType.FOLDER)