diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..361916a --- /dev/null +++ b/.env.example @@ -0,0 +1,42 @@ +API_BASE_URL=https://api.onspring.com +SANDBOX_API_KEY=NEEDS_TO_BE_SET + +# Apps +TEST_APP_ID=NEEDS_TO_BE_SET +TEST_SURVEY_ID=NEEDS_TO_BE_SET +TEST_APP_ID_NO_ACCESS=NEEDS_TO_BE_SET +TEST_APP_IDS=NEEDS_TO_BE_SET +TEST_APP_IDS_NO_ACCESS=NEEDS_TO_BE_SET + +# Fields +TEST_FIELD_ID=NEEDS_TO_BE_SET +TEST_FIELD_ID_NO_ACCESS=NEEDS_TO_BE_SET +TEST_FIELD_IDS=NEEDS_TO_BE_SET +TEST_FIELD_IDS_NO_ACCESS=NEEDS_TO_BE_SET + +# Records +TEST_RECORD=NEEDS_TO_BE_SET +TEST_SURVEY_RECORD_ID=NEEDS_TO_BE_SET +TEST_TEXT_FIELD=NEEDS_TO_BE_SET + +# Files — Attachment +TEST_ATTACHMENT_FIELD=NEEDS_TO_BE_SET +TEST_ATTACHMENT_FIELD_NO_ACCESS_FIELD=NEEDS_TO_BE_SET +TEST_ATTACHMENT_FIELD_NO_ACCESS_APP=NEEDS_TO_BE_SET +TEST_ATTACHMENT=NEEDS_TO_BE_SET + +# Files — Image +TEST_IMAGE_FIELD=NEEDS_TO_BE_SET +TEST_IMAGE=NEEDS_TO_BE_SET + +# Lists +TEST_LIST_FIELD=NEEDS_TO_BE_SET +TEST_LIST_FIELD_NO_ACCESS=NEEDS_TO_BE_SET +TEST_LIST_ID=NEEDS_TO_BE_SET +TEST_LIST_ID_NO_ACCESS=NEEDS_TO_BE_SET +TEST_LIST_ITEM_ID_NO_ACCESS=NEEDS_TO_BE_SET + +# Reports +TEST_REPORT=NEEDS_TO_BE_SET +TEST_REPORT_NO_ACCESS=NEEDS_TO_BE_SET +TEST_REPORT_WITH_CHART_DATA=NEEDS_TO_BE_SET diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4793df1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,176 @@ +from pathlib import Path + +import pytest + +from onspring_api_sdk import AsyncOnspringClient, OnspringClient + +TEST_URL = "https://test.com" +TEST_API_KEY = "apiKey" + + +@pytest.fixture +def client(): + return OnspringClient(TEST_URL, TEST_API_KEY) + + +@pytest.fixture +def async_client(): + return AsyncOnspringClient(TEST_URL, TEST_API_KEY) + + +MOCK_APP = { + "href": "https://api.onspring.com/Apps/id/1", + "id": 1, + "name": "Test App", +} + +MOCK_APPS_RESPONSE = { + "pageNumber": 1, + "pageSize": 50, + "totalPages": 1, + "totalRecords": 2, + "items": [ + MOCK_APP, + {**MOCK_APP, "id": 2, "name": "App 2"}, + ], +} + +MOCK_APPS_BATCH_RESPONSE = { + "count": 2, + "items": [MOCK_APP, {**MOCK_APP, "id": 2, "name": "App 2"}], +} + + +MOCK_FIELD = { + "id": 1, + "appId": 10, + "name": "Test Field", + "type": "Text", + "status": "Enabled", + "isRequired": True, + "isUnique": False, +} + +MOCK_LIST_FIELD = { + "id": 2, + "appId": 10, + "name": "List Field", + "type": "List", + "status": "Enabled", + "isRequired": False, + "isUnique": False, + "multiplicity": "SingleSelect", + "listId": 100, + "values": [ + { + "id": "2c1af5b1-0f90-4378-b9a5-8b7e22f2bc84", + "name": "list_value_1", + "sortOrder": 1, + "numericValue": 1, + "color": "#008e8e", + }, + ], +} + +MOCK_FIELDS_RESPONSE = { + "pageNumber": 1, + "pageSize": 50, + "totalPages": 1, + "totalRecords": 2, + "items": [MOCK_FIELD, MOCK_LIST_FIELD], +} + +MOCK_FIELDS_BATCH_RESPONSE = { + "count": 2, + "items": [MOCK_FIELD, MOCK_LIST_FIELD], +} + + +MOCK_FILE_INFO = { + "type": "Attachment", + "contentType": "text/plain", + "name": "test.txt", + "createdDate": "2024-01-01T00:00:00", + "modifiedDate": "2024-01-01T00:00:00", + "owner": "Test User", + "notes": "Test note", + "fileHref": "https://api.onspring.com/Files/...", +} + +MOCK_SAVE_FILE_RESPONSE = {"id": 1} + + +MOCK_LIST_ITEM_RESPONSE = {"id": "00000000-0000-0000-0000-000000000000"} + + +MOCK_RECORD = { + "appId": 100, + "recordId": 1, + "fieldData": [ + {"fieldId": 1, "value": "Test Value", "type": "String"}, + {"fieldId": 2, "value": 42, "type": "Integer"}, + ], +} + +MOCK_RECORDS_RESPONSE = { + "pageNumber": 1, + "pageSize": 50, + "totalPages": 1, + "totalRecords": 1, + "items": [MOCK_RECORD], +} + +MOCK_RECORDS_BATCH_RESPONSE = { + "count": 1, + "items": [MOCK_RECORD], +} + +MOCK_SAVE_RECORD_RESPONSE = { + "id": 1, + "warnings": [], +} + + +MOCK_REPORT_RESPONSE = { + "columns": ["col1", "col2"], + "rows": [ + {"recordId": 1, "cells": ["a", "b"]}, + ], +} + +MOCK_REPORT_BY_APP = { + "appId": 10, + "id": 53, + "name": "Test Report", + "description": "A test report", +} + +MOCK_REPORTS_BY_APP_RESPONSE = { + "pageNumber": 1, + "pageSize": 50, + "totalPages": 1, + "totalRecords": 1, + "items": [MOCK_REPORT_BY_APP], +} + + +MOCK_MESSAGE_RESPONSE = {"message": "An error occurred"} + + +TEMP_DIR = Path(__file__).parent / "_temp" + + +@pytest.fixture(autouse=True) +def manage_temp_dir(): + TEMP_DIR.mkdir(exist_ok=True) + yield + if TEMP_DIR.exists(): + for f in TEMP_DIR.iterdir(): + f.unlink() + TEMP_DIR.rmdir() + + +def create_temp_file(name: str = "test.txt", content: bytes = b"Hello World!") -> Path: + path = TEMP_DIR / name + path.write_bytes(content) + return path diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000..6c7468c --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,188 @@ +import os +from pathlib import Path + +import pytest +from dotenv import load_dotenv + +from onspring_api_sdk import AsyncOnspringClient, OnspringClient + +load_dotenv() + + +def _required_env(key: str) -> str: + value = os.environ.get(key) + if value is None: + pytest.fail(f"{key} is not defined") + return value + + +def _optional_env(key: str) -> str | None: + return os.environ.get(key) + + +def pytest_configure(config): + config.addinivalue_line("markers", "integration: marks tests as integration tests (real API calls)") + + +@pytest.fixture(scope="session") +def base_url(): + return _required_env("API_BASE_URL") + + +@pytest.fixture(scope="session") +def api_key(): + return _required_env("SANDBOX_API_KEY") + + +@pytest.fixture(scope="session") +def test_app_id(): + return int(_required_env("TEST_APP_ID")) + + +@pytest.fixture(scope="session") +def test_survey_id(): + return int(_required_env("TEST_SURVEY_ID")) + + +@pytest.fixture(scope="session") +def test_app_id_no_access(): + return int(_required_env("TEST_APP_ID_NO_ACCESS")) + + +@pytest.fixture(scope="session") +def test_app_ids(): + raw = _required_env("TEST_APP_IDS") + return [int(x) for x in raw.split(",")] + + +@pytest.fixture(scope="session") +def test_app_ids_no_access(): + raw = _required_env("TEST_APP_IDS_NO_ACCESS") + return [int(x) for x in raw.split(",")] + + +@pytest.fixture(scope="session") +def test_field_id(): + return int(_required_env("TEST_FIELD_ID")) + + +@pytest.fixture(scope="session") +def test_field_id_no_access(): + return int(_required_env("TEST_FIELD_ID_NO_ACCESS")) + + +@pytest.fixture(scope="session") +def test_field_ids(): + raw = _required_env("TEST_FIELD_IDS") + return [int(x) for x in raw.split(",")] + + +@pytest.fixture(scope="session") +def test_field_ids_no_access(): + raw = _required_env("TEST_FIELD_IDS_NO_ACCESS") + return [int(x) for x in raw.split(",")] + + +@pytest.fixture(scope="session") +def test_record(): + return int(_required_env("TEST_RECORD")) + + +@pytest.fixture(scope="session") +def test_survey_record_id(): + return int(_required_env("TEST_SURVEY_RECORD_ID")) + + +@pytest.fixture(scope="session") +def test_text_field(): + return int(_required_env("TEST_TEXT_FIELD")) + + +@pytest.fixture(scope="session") +def test_attachment_field(): + return int(_required_env("TEST_ATTACHMENT_FIELD")) + + +@pytest.fixture(scope="session") +def test_attachment_field_no_access_field(): + return int(_required_env("TEST_ATTACHMENT_FIELD_NO_ACCESS_FIELD")) + + +@pytest.fixture(scope="session") +def test_attachment_field_no_access_app(): + return int(_required_env("TEST_ATTACHMENT_FIELD_NO_ACCESS_APP")) + + +@pytest.fixture(scope="session") +def test_attachment(): + return int(_required_env("TEST_ATTACHMENT")) + + +@pytest.fixture(scope="session") +def test_image_field(): + return int(_required_env("TEST_IMAGE_FIELD")) + + +@pytest.fixture(scope="session") +def test_image(): + return int(_required_env("TEST_IMAGE")) + + +@pytest.fixture(scope="session") +def test_list_field(): + return int(_required_env("TEST_LIST_FIELD")) + + +@pytest.fixture(scope="session") +def test_list_field_no_access(): + return int(_required_env("TEST_LIST_FIELD_NO_ACCESS")) + + +@pytest.fixture(scope="session") +def test_list_id(): + return int(_required_env("TEST_LIST_ID")) + + +@pytest.fixture(scope="session") +def test_list_id_no_access(): + return int(_required_env("TEST_LIST_ID_NO_ACCESS")) + + +@pytest.fixture(scope="session") +def test_list_item_id_no_access(): + return _required_env("TEST_LIST_ITEM_ID_NO_ACCESS") + + +@pytest.fixture(scope="session") +def test_report(): + return int(_required_env("TEST_REPORT")) + + +@pytest.fixture(scope="session") +def test_report_no_access(): + return int(_required_env("TEST_REPORT_NO_ACCESS")) + + +@pytest.fixture(scope="session") +def test_report_with_chart_data(): + return int(_required_env("TEST_REPORT_WITH_CHART_DATA")) + + +@pytest.fixture(scope="session") +def test_survey_auto_number_field(): + return _optional_env("TEST_SURVEY_AUTO_NUMBER_FIELD") + + +@pytest.fixture(scope="session") +def testdata_dir(): + return Path(__file__).parent / "testdata" + + +@pytest.fixture +def client(base_url, api_key): + return OnspringClient(base_url, api_key) + + +@pytest.fixture +def async_client(base_url, api_key): + return AsyncOnspringClient(base_url, api_key) diff --git a/tests/integration/test_apps.py b/tests/integration/test_apps.py new file mode 100644 index 0000000..b4802d5 --- /dev/null +++ b/tests/integration/test_apps.py @@ -0,0 +1,243 @@ +import pytest + +from onspring_api_sdk.models import PagingRequest + +pytestmark = pytest.mark.integration + + +class TestGetApps: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client): + response = client.get_apps() + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.page_number is not None + assert response.data.page_size is not None + assert response.data.total_pages is not None + assert response.data.total_records is not None + assert response.data.apps is not None + assert len(response.data.apps) > 0 + + for app in response.data.apps: + assert app.id is not None + assert app.name is not None + assert app.href is not None + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client): + response = await async_client.get_apps() + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert len(response.data.apps) > 0 + + @pytest.mark.flaky(reruns=3) + def test_with_paging_request(self, client): + response = client.get_apps(paging_request=PagingRequest(page_number=1, page_size=1)) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.page_number == 1 + assert response.data.page_size == 1 + assert len(response.data.apps) <= 1 + + @pytest.mark.flaky(reruns=3) + async def test_with_paging_request_async(self, async_client): + response = await async_client.get_apps(paging_request=PagingRequest(page_number=1, page_size=1)) + + assert response.status_code == 200 + assert response.data is not None + assert response.data.page_number == 1 + assert response.data.page_size == 1 + assert len(response.data.apps) <= 1 + + @pytest.mark.flaky(reruns=3) + def test_invalid_page_size_returns_400(self, client): + response = client.get_apps(paging_request=PagingRequest(page_number=1, page_size=-1)) + + assert response.status_code == 400 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_page_size_returns_400_async(self, async_client): + response = await async_client.get_apps(paging_request=PagingRequest(page_number=1, page_size=-1)) + + assert response.status_code == 400 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + response = bad.get_apps() + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async(self, base_url): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + response = await bad.get_apps() + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + +class TestGetAppById: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_app_id): + response = client.get_app_by_id(app_id=test_app_id) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.app.id == test_app_id + assert response.data.app.name is not None + assert response.data.app.href is not None + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_app_id): + response = await async_client.get_app_by_id(app_id=test_app_id) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.app.id == test_app_id + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + response = bad.get_app_by_id(app_id=1) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async(self, base_url): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + response = await bad.get_app_by_id(app_id=1) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_app_id_no_access): + response = client.get_app_by_id(app_id=test_app_id_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_access_returns_403_async(self, async_client, test_app_id_no_access): + response = await async_client.get_app_by_id(app_id=test_app_id_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_not_found_returns_404(self, client): + response = client.get_app_by_id(app_id=0) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_not_found_returns_404_async(self, async_client): + response = await async_client.get_app_by_id(app_id=0) + + assert response.status_code == 404 + assert not response.is_successful + assert response.data is None + + +class TestGetAppsByIds: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_app_ids): + response = client.get_apps_by_ids(app_ids=test_app_ids) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.apps is not None + assert len(response.data.apps) == len(test_app_ids) + + for app in response.data.apps: + assert app.id is not None + assert app.name is not None + assert app.href is not None + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_app_ids): + response = await async_client.get_apps_by_ids(app_ids=test_app_ids) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert len(response.data.apps) == len(test_app_ids) + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + response = bad.get_apps_by_ids(app_ids=[1]) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async(self, base_url): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + response = await bad.get_apps_by_ids(app_ids=[1]) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_app_ids_no_access): + response = client.get_apps_by_ids(app_ids=test_app_ids_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_access_returns_403_async(self, async_client, test_app_ids_no_access): + response = await async_client.get_apps_by_ids(app_ids=test_app_ids_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None diff --git a/tests/integration/test_fields.py b/tests/integration/test_fields.py new file mode 100644 index 0000000..d7a6fb2 --- /dev/null +++ b/tests/integration/test_fields.py @@ -0,0 +1,284 @@ +import pytest + +from onspring_api_sdk.models import PagingRequest + +pytestmark = pytest.mark.integration + + +class TestGetFieldById: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_field_id): + response = client.get_field_by_id(field_id=test_field_id) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.field.id == test_field_id + assert response.data.field.name is not None + assert response.data.field.app_id is not None + assert response.data.field.type is not None + assert response.data.field.status is not None + assert response.data.field.is_required is not None + assert response.data.field.is_unique is not None + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_field_id): + response = await async_client.get_field_by_id(field_id=test_field_id) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.field.id == test_field_id + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + response = bad.get_field_by_id(field_id=1) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async(self, base_url): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + response = await bad.get_field_by_id(field_id=1) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_field_id_no_access): + response = client.get_field_by_id(field_id=test_field_id_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_access_returns_403_async(self, async_client, test_field_id_no_access): + response = await async_client.get_field_by_id(field_id=test_field_id_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_not_found_returns_404(self, client): + response = client.get_field_by_id(field_id=0) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_not_found_returns_404_async(self, async_client): + response = await async_client.get_field_by_id(field_id=0) + + assert response.status_code == 404 + assert not response.is_successful + assert response.data is None + + +class TestGetFieldsByAppId: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_survey_id): + response = client.get_fields_by_app_id(app_id=test_survey_id) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.page_number is not None + assert response.data.page_size is not None + assert response.data.total_pages is not None + assert response.data.total_records is not None + assert response.data.fields is not None + assert len(response.data.fields) > 0 + + for field in response.data.fields: + assert field.id is not None + assert field.name is not None + assert field.app_id is not None + assert field.type is not None + assert field.status is not None + assert field.is_required is not None + assert field.is_unique is not None + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_survey_id): + response = await async_client.get_fields_by_app_id(app_id=test_survey_id) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert len(response.data.fields) > 0 + + @pytest.mark.flaky(reruns=3) + def test_with_paging_request(self, client, test_survey_id): + response = client.get_fields_by_app_id( + app_id=test_survey_id, + paging_request=PagingRequest(page_number=1, page_size=1), + ) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.page_number == 1 + assert response.data.page_size == 1 + assert len(response.data.fields) <= 1 + + @pytest.mark.flaky(reruns=3) + async def test_with_paging_request_async(self, async_client, test_survey_id): + response = await async_client.get_fields_by_app_id( + app_id=test_survey_id, + paging_request=PagingRequest(page_number=1, page_size=1), + ) + + assert response.status_code == 200 + assert response.data is not None + assert response.data.page_number == 1 + assert response.data.page_size == 1 + assert len(response.data.fields) <= 1 + + @pytest.mark.flaky(reruns=3) + def test_invalid_page_size_returns_400(self, client, test_survey_id): + response = client.get_fields_by_app_id( + app_id=test_survey_id, + paging_request=PagingRequest(page_number=1, page_size=-1), + ) + + assert response.status_code == 400 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_page_size_returns_400_async(self, async_client, test_survey_id): + response = await async_client.get_fields_by_app_id( + app_id=test_survey_id, + paging_request=PagingRequest(page_number=1, page_size=-1), + ) + + assert response.status_code == 400 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + response = bad.get_fields_by_app_id(app_id=1) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async(self, base_url): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + response = await bad.get_fields_by_app_id(app_id=1) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_app_id_no_access): + response = client.get_fields_by_app_id(app_id=test_app_id_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_access_returns_403_async(self, async_client, test_app_id_no_access): + response = await async_client.get_fields_by_app_id(app_id=test_app_id_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None + + +class TestGetFieldsByIds: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_field_ids): + response = client.get_fields_by_ids(field_ids=test_field_ids) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.count == len(test_field_ids) + assert len(response.data.fields) == len(test_field_ids) + + for field in response.data.fields: + assert field.id is not None + assert field.name is not None + assert field.app_id is not None + assert field.type is not None + assert field.status is not None + assert field.is_required is not None + assert field.is_unique is not None + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_field_ids): + response = await async_client.get_fields_by_ids(field_ids=test_field_ids) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.count == len(test_field_ids) + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + response = bad.get_fields_by_ids(field_ids=[1, 2, 3]) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async(self, base_url): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + response = await bad.get_fields_by_ids(field_ids=[1, 2, 3]) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_field_ids_no_access): + response = client.get_fields_by_ids(field_ids=test_field_ids_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_access_returns_403_async(self, async_client, test_field_ids_no_access): + response = await async_client.get_fields_by_ids(field_ids=test_field_ids_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None diff --git a/tests/integration/test_files.py b/tests/integration/test_files.py new file mode 100644 index 0000000..a6f8fa6 --- /dev/null +++ b/tests/integration/test_files.py @@ -0,0 +1,679 @@ +import pytest + +from onspring_api_sdk.models import SaveFileRequest + +pytestmark = pytest.mark.integration + + +def _attachment_path(testdata_dir): + return str(testdata_dir / "test-attachment.txt") + + +def _image_path(testdata_dir): + return str(testdata_dir / "test-image.jpeg") + + +class TestGetFileInfoById: + @pytest.mark.flaky(reruns=3) + def test_attachment_field(self, client, test_record, test_attachment_field, test_attachment): + response = client.get_file_info_by_id( + record_id=test_record, + field_id=test_attachment_field, + file_id=test_attachment, + ) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.file_info.created_date is not None + assert response.data.file_info.content_type is not None + assert response.data.file_info.file_href is not None + assert response.data.file_info.name is not None + assert response.data.file_info.modified_date is not None + assert response.data.file_info.type is not None + assert response.data.file_info.owner is not None + + @pytest.mark.flaky(reruns=3) + async def test_attachment_field_async(self, async_client, test_record, test_attachment_field, test_attachment): + response = await async_client.get_file_info_by_id( + record_id=test_record, + field_id=test_attachment_field, + file_id=test_attachment, + ) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + + @pytest.mark.flaky(reruns=3) + def test_image_field(self, client, test_record, test_image_field, test_image): + response = client.get_file_info_by_id( + record_id=test_record, + field_id=test_image_field, + file_id=test_image, + ) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.file_info.created_date is not None + assert response.data.file_info.content_type is not None + assert response.data.file_info.file_href is not None + assert response.data.file_info.name is not None + assert response.data.file_info.modified_date is not None + assert response.data.file_info.type is not None + assert response.data.file_info.owner is not None + + @pytest.mark.flaky(reruns=3) + async def test_image_field_async(self, async_client, test_record, test_image_field, test_image): + response = await async_client.get_file_info_by_id( + record_id=test_record, + field_id=test_image_field, + file_id=test_image, + ) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + + @pytest.mark.flaky(reruns=3) + def test_non_file_field_returns_400(self, client, test_record, test_text_field, test_attachment): + response = client.get_file_info_by_id( + record_id=test_record, + field_id=test_text_field, + file_id=test_attachment, + ) + + assert response.status_code == 400 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_non_file_field_returns_400_async(self, async_client, test_record, test_text_field, test_attachment): + response = await async_client.get_file_info_by_id( + record_id=test_record, + field_id=test_text_field, + file_id=test_attachment, + ) + + assert response.status_code == 400 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url, test_record, test_attachment_field, test_attachment): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + response = bad.get_file_info_by_id( + record_id=test_record, + field_id=test_attachment_field, + file_id=test_attachment, + ) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async( + self, base_url, test_record, test_attachment_field, test_attachment + ): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + response = await bad.get_file_info_by_id( + record_id=test_record, + field_id=test_attachment_field, + file_id=test_attachment, + ) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_field_access_returns_403(self, client, test_attachment_field_no_access_field): + response = client.get_file_info_by_id(record_id=1, field_id=test_attachment_field_no_access_field, file_id=1) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_field_access_returns_403_async(self, async_client, test_attachment_field_no_access_field): + response = await async_client.get_file_info_by_id( + record_id=1, field_id=test_attachment_field_no_access_field, file_id=1 + ) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_app_access_returns_403(self, client, test_attachment_field_no_access_app): + response = client.get_file_info_by_id(record_id=1, field_id=test_attachment_field_no_access_app, file_id=1) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_app_access_returns_403_async(self, async_client, test_attachment_field_no_access_app): + response = await async_client.get_file_info_by_id( + record_id=1, field_id=test_attachment_field_no_access_app, file_id=1 + ) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_field_not_found_returns_404(self, client): + response = client.get_file_info_by_id(record_id=1, field_id=0, file_id=1) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_field_not_found_returns_404_async(self, async_client): + response = await async_client.get_file_info_by_id(record_id=1, field_id=0, file_id=1) + + assert response.status_code == 404 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_record_not_found_returns_404(self, client, test_attachment_field): + response = client.get_file_info_by_id(record_id=0, field_id=test_attachment_field, file_id=1) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_record_not_found_returns_404_async(self, async_client, test_attachment_field): + response = await async_client.get_file_info_by_id(record_id=0, field_id=test_attachment_field, file_id=1) + + assert response.status_code == 404 + assert not response.is_successful + assert response.data is None + + +class TestGetFileById: + @pytest.mark.flaky(reruns=3) + def test_attachment_field(self, client, test_record, test_attachment_field, test_attachment): + response = client.get_file_by_id(record_id=test_record, field_id=test_attachment_field, file_id=test_attachment) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.file.content_length is not None + assert response.data.file.content_type is not None + assert response.data.file.name is not None + assert response.data.file.content is not None + + @pytest.mark.flaky(reruns=3) + async def test_attachment_field_async(self, async_client, test_record, test_attachment_field, test_attachment): + response = await async_client.get_file_by_id( + record_id=test_record, field_id=test_attachment_field, file_id=test_attachment + ) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + + @pytest.mark.flaky(reruns=3) + def test_image_field(self, client, test_record, test_image_field, test_image): + response = client.get_file_by_id(record_id=test_record, field_id=test_image_field, file_id=test_image) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.file.content_length is not None + assert response.data.file.content_type is not None + assert response.data.file.name is not None + assert response.data.file.content is not None + + @pytest.mark.flaky(reruns=3) + async def test_image_field_async(self, async_client, test_record, test_image_field, test_image): + response = await async_client.get_file_by_id( + record_id=test_record, field_id=test_image_field, file_id=test_image + ) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + + @pytest.mark.flaky(reruns=3) + def test_non_file_field_returns_400(self, client, test_record, test_text_field, test_attachment): + response = client.get_file_by_id(record_id=test_record, field_id=test_text_field, file_id=test_attachment) + + assert response.status_code == 400 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_non_file_field_returns_400_async(self, async_client, test_record, test_text_field, test_attachment): + response = await async_client.get_file_by_id( + record_id=test_record, field_id=test_text_field, file_id=test_attachment + ) + + assert response.status_code == 400 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url, test_record, test_attachment_field, test_attachment): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + response = bad.get_file_by_id(record_id=test_record, field_id=test_attachment_field, file_id=test_attachment) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async( + self, base_url, test_record, test_attachment_field, test_attachment + ): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + response = await bad.get_file_by_id( + record_id=test_record, field_id=test_attachment_field, file_id=test_attachment + ) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_field_access_returns_403(self, client, test_attachment_field_no_access_field): + response = client.get_file_by_id(record_id=1, field_id=test_attachment_field_no_access_field, file_id=1) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_field_access_returns_403_async(self, async_client, test_attachment_field_no_access_field): + response = await async_client.get_file_by_id( + record_id=1, field_id=test_attachment_field_no_access_field, file_id=1 + ) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_app_access_returns_403(self, client, test_attachment_field_no_access_app): + response = client.get_file_by_id(record_id=1, field_id=test_attachment_field_no_access_app, file_id=1) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_app_access_returns_403_async(self, async_client, test_attachment_field_no_access_app): + response = await async_client.get_file_by_id( + record_id=1, field_id=test_attachment_field_no_access_app, file_id=1 + ) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_field_not_found_returns_404(self, client): + response = client.get_file_by_id(record_id=1, field_id=0, file_id=1) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_field_not_found_returns_404_async(self, async_client): + response = await async_client.get_file_by_id(record_id=1, field_id=0, file_id=1) + + assert response.status_code == 404 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_record_not_found_returns_404(self, client, test_attachment_field): + response = client.get_file_by_id(record_id=0, field_id=test_attachment_field, file_id=1) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_record_not_found_returns_404_async(self, async_client, test_attachment_field): + response = await async_client.get_file_by_id(record_id=0, field_id=test_attachment_field, file_id=1) + + assert response.status_code == 404 + assert not response.is_successful + assert response.data is None + + +class TestSaveFile: + _attachment_file_ids: list[int] = [] + + @pytest.fixture(autouse=True) + def cleanup_attachments(self, client, test_record, test_attachment_field): + yield + + for file_id in self._attachment_file_ids: + client.delete_file_by_id(record_id=test_record, field_id=test_attachment_field, file_id=file_id) + + self._attachment_file_ids.clear() + + @pytest.mark.flaky(reruns=3) + def test_attachment_field(self, client, test_record, test_attachment_field, testdata_dir): + request = SaveFileRequest( + record_id=test_record, + field_id=test_attachment_field, + file_name="test-attachment.txt", + file_path=_attachment_path(testdata_dir), + content_type="text/plain", + notes="integration test", + ) + + response = client.save_file(request) + + assert response.status_code == 201 + assert response.is_successful + assert response.data is not None + assert response.data.id is not None + + self._attachment_file_ids.append(response.data.id) + + @pytest.mark.flaky(reruns=3) + async def test_attachment_field_async(self, async_client, test_record, test_attachment_field, testdata_dir): + request = SaveFileRequest( + record_id=test_record, + field_id=test_attachment_field, + file_name="test-attachment.txt", + file_path=_attachment_path(testdata_dir), + content_type="text/plain", + notes="integration test", + ) + + response = await async_client.save_file(request) + + assert response.status_code == 201 + assert response.is_successful + assert response.data is not None + assert response.data.id is not None + + self._attachment_file_ids.append(response.data.id) + + @pytest.mark.flaky(reruns=3) + def test_image_field(self, client, test_record, test_image_field, testdata_dir): + request = SaveFileRequest( + record_id=test_record, + field_id=test_image_field, + file_name="test-image.jpeg", + file_path=_image_path(testdata_dir), + content_type="image/jpeg", + notes="integration test", + ) + + response = client.save_file(request) + + assert response.status_code == 201 + assert response.is_successful + assert response.data is not None + assert response.data.id is not None + + self._attachment_file_ids.append(response.data.id) + + @pytest.mark.flaky(reruns=3) + def test_non_file_field_returns_400(self, client, test_record, test_text_field, testdata_dir): + request = SaveFileRequest( + record_id=test_record, + field_id=test_text_field, + file_name="test-attachment.txt", + file_path=_attachment_path(testdata_dir), + content_type="text/plain", + notes="integration test", + ) + + response = client.save_file(request) + + assert response.status_code == 400 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url, testdata_dir): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + request = SaveFileRequest( + record_id=1, + field_id=1, + file_name="test-attachment.txt", + file_path=_attachment_path(testdata_dir), + content_type="text/plain", + ) + + response = bad.save_file(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_field_access_returns_403(self, client, test_attachment_field_no_access_field, testdata_dir): + request = SaveFileRequest( + record_id=1, + field_id=test_attachment_field_no_access_field, + file_name="test-attachment.txt", + file_path=_attachment_path(testdata_dir), + content_type="text/plain", + ) + + response = client.save_file(request) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_app_access_returns_403(self, client, test_attachment_field_no_access_app, testdata_dir): + request = SaveFileRequest( + record_id=1, + field_id=test_attachment_field_no_access_app, + file_name="test-attachment.txt", + file_path=_attachment_path(testdata_dir), + content_type="text/plain", + ) + + response = client.save_file(request) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_field_not_found_returns_404(self, client, testdata_dir): + request = SaveFileRequest( + record_id=1, + field_id=0, + file_name="test-attachment.txt", + file_path=_attachment_path(testdata_dir), + content_type="text/plain", + ) + + response = client.save_file(request) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_record_not_found_returns_404(self, client, test_attachment_field, testdata_dir): + request = SaveFileRequest( + record_id=0, + field_id=test_attachment_field, + file_name="test-attachment.txt", + file_path=_attachment_path(testdata_dir), + content_type="text/plain", + ) + + response = client.save_file(request) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + +class TestDeleteFileById: + _attachment_file_id: int | None = None + _image_file_id: int | None = None + + @pytest.fixture(autouse=True) + def setup_files(self, client, test_record, test_attachment_field, test_image_field, testdata_dir): + if self._attachment_file_id is None: + req = SaveFileRequest( + record_id=test_record, + field_id=test_attachment_field, + file_name="test-attachment.txt", + file_path=_attachment_path(testdata_dir), + content_type="text/plain", + notes="delete test", + ) + + resp = client.save_file(req) + + if resp.data is not None: + self._attachment_file_id = resp.data.id + + if self._image_file_id is None: + req = SaveFileRequest( + record_id=test_record, + field_id=test_image_field, + file_name="test-image.jpeg", + file_path=_image_path(testdata_dir), + content_type="image/jpeg", + notes="delete test", + ) + + resp = client.save_file(req) + + if resp.data is not None: + self._image_file_id = resp.data.id + + yield + + @pytest.mark.flaky(reruns=3) + def test_attachment_field(self, client, test_record, test_attachment_field): + assert self._attachment_file_id is not None + + response = client.delete_file_by_id( + record_id=test_record, + field_id=test_attachment_field, + file_id=self._attachment_file_id, + ) + + assert response.status_code == 204 + assert response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_image_field(self, client, test_record, test_image_field): + assert self._image_file_id is not None + + response = client.delete_file_by_id( + record_id=test_record, + field_id=test_image_field, + file_id=self._image_file_id, + ) + + assert response.status_code == 204 + assert response.is_successful + assert response.message is not None + + @pytest.mark.flaky(reruns=3) + def test_non_file_field_returns_400(self, client, test_record, test_text_field): + response = client.delete_file_by_id(record_id=test_record, field_id=test_text_field, file_id=1) + + assert response.status_code == 400 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + response = bad.delete_file_by_id(record_id=1, field_id=1, file_id=1) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_field_access_returns_403(self, client, test_attachment_field_no_access_field): + response = client.delete_file_by_id(record_id=1, field_id=test_attachment_field_no_access_field, file_id=1) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_app_access_returns_403(self, client, test_attachment_field_no_access_app): + response = client.delete_file_by_id(record_id=1, field_id=test_attachment_field_no_access_app, file_id=1) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_field_not_found_returns_404(self, client): + response = client.delete_file_by_id(record_id=1, field_id=0, file_id=1) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_record_not_found_returns_404(self, client, test_attachment_field): + response = client.delete_file_by_id(record_id=0, field_id=test_attachment_field, file_id=1) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None diff --git a/tests/integration/test_lists.py b/tests/integration/test_lists.py new file mode 100644 index 0000000..b5b9571 --- /dev/null +++ b/tests/integration/test_lists.py @@ -0,0 +1,277 @@ +import uuid + +import pytest + +from onspring_api_sdk.models import ListItemRequest + +pytestmark = pytest.mark.integration + + +def _unique_name(): + return f"test_list_value_{uuid.uuid4().hex[:8]}" + + +class TestAddOrUpdateListItem: + _new_item_ids: list[uuid.UUID] = [] + + @pytest.fixture(autouse=True) + def cleanup(self, client, test_list_id): + yield + + for item_id in self._new_item_ids: + client.delete_list_item(list_id=test_list_id, item_id=str(item_id)) + + self._new_item_ids.clear() + + @pytest.mark.flaky(reruns=3) + def test_add(self, client, test_list_id): + request = ListItemRequest( + list_id=test_list_id, + name=_unique_name(), + numeric_value=1, + color="#000000", + ) + + response = client.add_or_update_list_item(request) + + assert response.status_code == 201 + assert response.is_successful + assert response.message is not None + assert response.data is not None + assert response.data.id is not None + + self._new_item_ids.append(response.data.id) + + @pytest.mark.flaky(reruns=3) + async def test_add_async(self, async_client, test_list_id): + request = ListItemRequest( + list_id=test_list_id, + name=_unique_name(), + numeric_value=1, + color="#000000", + ) + + response = await async_client.add_or_update_list_item(request) + + assert response.status_code == 201 + assert response.is_successful + assert response.data is not None + assert response.data.id is not None + + if response.data.id: + import os + + from onspring_api_sdk import OnspringClient + + sync = OnspringClient(os.environ["API_BASE_URL"], os.environ["SANDBOX_API_KEY"]) + sync.delete_list_item(list_id=test_list_id, item_id=str(response.data.id)) + + @pytest.mark.flaky(reruns=3) + def test_update(self, client, test_list_id): + add_request = ListItemRequest( + list_id=test_list_id, + name=_unique_name(), + numeric_value=1, + color="#000000", + ) + + add_response = client.add_or_update_list_item(add_request) + + assert add_response.data is not None + + item_id = add_response.data.id + self._new_item_ids.append(item_id) + + update_request = ListItemRequest( + list_id=test_list_id, + name=_unique_name(), + id=item_id, + numeric_value=1, + color="#000000", + ) + + update_response = client.add_or_update_list_item(update_request) + + assert update_response.status_code == 200 + assert update_response.is_successful + assert update_response.message is not None + assert update_response.data is not None + + @pytest.mark.flaky(reruns=3) + async def test_update_async(self, async_client, test_list_id): + add_request = ListItemRequest( + list_id=test_list_id, + name=_unique_name(), + numeric_value=1, + color="#000000", + ) + + add_response = await async_client.add_or_update_list_item(add_request) + + assert add_response.data is not None + + item_id = add_response.data.id + + import os + + from onspring_api_sdk import OnspringClient + + sync = OnspringClient(os.environ["API_BASE_URL"], os.environ["SANDBOX_API_KEY"]) + + update_request = ListItemRequest( + list_id=test_list_id, + name=_unique_name(), + id=item_id, + numeric_value=1, + color="#000000", + ) + + update_response = await async_client.add_or_update_list_item(update_request) + + assert update_response.status_code == 200 + assert update_response.is_successful + assert update_response.data is not None + + sync.delete_list_item(list_id=test_list_id, item_id=str(item_id)) + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + request = ListItemRequest(list_id=1, name="test", numeric_value=1, color="#000000") + + response = bad.add_or_update_list_item(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client): + request = ListItemRequest(list_id=1, name="test", numeric_value=1, color="#000000") + + response = client.add_or_update_list_item(request) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_list_not_found_returns_404(self, client): + request = ListItemRequest(list_id=0, name="test", numeric_value=1, color="#000000") + + response = client.add_or_update_list_item(request) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_item_not_found_returns_404(self, client, test_list_id): + request = ListItemRequest( + list_id=test_list_id, + id=uuid.UUID("3fa85f64-5717-4562-b3fc-2c963f66afa6"), + name="test", + ) + + response = client.add_or_update_list_item(request) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + +class TestDeleteListItem: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_list_id): + add_request = ListItemRequest( + list_id=test_list_id, + name=_unique_name(), + numeric_value=1, + color="#000000", + ) + + add_response = client.add_or_update_list_item(add_request) + + assert add_response.data is not None + + item_id = str(add_response.data.id) + + response = client.delete_list_item(list_id=test_list_id, item_id=item_id) + + assert response.status_code == 204 + assert response.is_successful + assert response.message is not None + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_list_id): + import os + + from onspring_api_sdk import OnspringClient + + sync = OnspringClient(os.environ["API_BASE_URL"], os.environ["SANDBOX_API_KEY"]) + + add_request = ListItemRequest( + list_id=test_list_id, + name=_unique_name(), + numeric_value=1, + color="#000000", + ) + + add_response = sync.add_or_update_list_item(add_request) + + assert add_response.data is not None + + item_id = str(add_response.data.id) + + response = await async_client.delete_list_item(list_id=test_list_id, item_id=item_id) + + assert response.status_code == 204 + assert response.is_successful + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + response = bad.delete_list_item(list_id=1, item_id="1") + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_list_id_no_access, test_list_item_id_no_access): + response = client.delete_list_item(list_id=test_list_id_no_access, item_id=test_list_item_id_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_list_not_found_returns_404(self, client): + response = client.delete_list_item(list_id=0, item_id="3fa85f64-5717-4562-b3fc-2c963f66afa6") + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_item_not_found_returns_404(self, client, test_list_id): + response = client.delete_list_item( + list_id=test_list_id, + item_id="3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None diff --git a/tests/integration/test_ping.py b/tests/integration/test_ping.py new file mode 100644 index 0000000..9c79428 --- /dev/null +++ b/tests/integration/test_ping.py @@ -0,0 +1,15 @@ +import pytest + +pytestmark = pytest.mark.integration + + +class TestCanConnect: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client): + result = client.can_connect() + assert result is True + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client): + result = await async_client.can_connect() + assert result is True diff --git a/tests/integration/test_records.py b/tests/integration/test_records.py new file mode 100644 index 0000000..16838b6 --- /dev/null +++ b/tests/integration/test_records.py @@ -0,0 +1,804 @@ +import pytest + +from onspring_api_sdk.enums import DataFormat +from onspring_api_sdk.models import ( + DeleteBatchRecordsRequest, + GetBatchRecordsRequest, + GetRecordByIdRequest, + GetRecordsByAppRequest, + QueryRecordsRequest, + Record, + StringFieldValue, +) + +pytestmark = pytest.mark.integration + + +class TestGetRecordsByAppId: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_survey_id): + request = GetRecordsByAppRequest(app_id=test_survey_id) + + response = client.get_records_by_app_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.page_number is not None + assert response.data.page_size is not None + assert response.data.total_pages is not None + assert response.data.total_records is not None + assert response.data.records is not None + + for record in response.data.records: + assert record.app_id == test_survey_id + assert record.record_id is not None + assert record.fields is not None + assert len(record.fields) > 0 + + for field in record.fields: + assert field.field_id is not None + assert field.type is not None + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_survey_id): + request = GetRecordsByAppRequest(app_id=test_survey_id) + + response = await async_client.get_records_by_app_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert len(response.data.records) > 0 + + @pytest.mark.flaky(reruns=3) + def test_with_params(self, client, test_survey_id, test_text_field): + request = GetRecordsByAppRequest( + app_id=test_survey_id, + field_ids=[test_text_field], + data_format=DataFormat.Formatted.name, + page_number=1, + page_size=1, + ) + + response = client.get_records_by_app_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.page_number == 1 + assert response.data.page_size == 1 + assert len(response.data.records) == 1 + + @pytest.mark.flaky(reruns=3) + async def test_with_params_async(self, async_client, test_survey_id, test_text_field): + request = GetRecordsByAppRequest( + app_id=test_survey_id, + field_ids=[test_text_field], + data_format=DataFormat.Formatted.name, + page_number=1, + page_size=1, + ) + + response = await async_client.get_records_by_app_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.page_number == 1 + assert response.data.page_size == 1 + assert len(response.data.records) == 1 + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + request = GetRecordsByAppRequest(app_id=0) + + response = bad.get_records_by_app_id(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async(self, base_url): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + request = GetRecordsByAppRequest(app_id=0) + + response = await bad.get_records_by_app_id(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_app_id_no_access): + request = GetRecordsByAppRequest(app_id=test_app_id_no_access) + + response = client.get_records_by_app_id(request) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_access_returns_403_async(self, async_client, test_app_id_no_access): + request = GetRecordsByAppRequest(app_id=test_app_id_no_access) + + response = await async_client.get_records_by_app_id(request) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None + + +class TestGetRecordById: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_survey_id, test_survey_record_id): + request = GetRecordByIdRequest(app_id=test_survey_id, record_id=test_survey_record_id) + + response = client.get_record_by_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.app_id == test_survey_id + assert response.data.record_id == test_survey_record_id + assert response.data.fields is not None + assert len(response.data.fields) > 0 + + for field in response.data.fields: + assert field.field_id is not None + assert field.type is not None + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_survey_id, test_survey_record_id): + request = GetRecordByIdRequest(app_id=test_survey_id, record_id=test_survey_record_id) + + response = await async_client.get_record_by_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.app_id == test_survey_id + assert response.data.record_id == test_survey_record_id + + @pytest.mark.flaky(reruns=3) + def test_with_params(self, client, test_survey_id, test_survey_record_id, test_text_field): + request = GetRecordByIdRequest( + app_id=test_survey_id, + record_id=test_survey_record_id, + field_ids=[test_text_field], + data_format=DataFormat.Formatted.name, + ) + + response = client.get_record_by_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.app_id == test_survey_id + assert response.data.record_id == test_survey_record_id + assert len(response.data.fields) > 0 + + @pytest.mark.flaky(reruns=3) + async def test_with_params_async(self, async_client, test_survey_id, test_survey_record_id, test_text_field): + request = GetRecordByIdRequest( + app_id=test_survey_id, + record_id=test_survey_record_id, + field_ids=[test_text_field], + data_format=DataFormat.Formatted.name, + ) + + response = await async_client.get_record_by_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + request = GetRecordByIdRequest(app_id=1, record_id=1) + + response = bad.get_record_by_id(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async(self, base_url): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + request = GetRecordByIdRequest(app_id=1, record_id=1) + + response = await bad.get_record_by_id(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_record_not_found_returns_404(self, client, test_survey_id): + request = GetRecordByIdRequest(app_id=test_survey_id, record_id=0) + + response = client.get_record_by_id(request) + + assert response.status_code == 404 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_record_not_found_returns_404_async(self, async_client, test_survey_id): + request = GetRecordByIdRequest(app_id=test_survey_id, record_id=0) + + response = await async_client.get_record_by_id(request) + + assert response.status_code == 404 + assert not response.is_successful + assert response.data is None + + +class TestSaveRecord: + _new_records: list[dict] = [] + + @pytest.fixture(autouse=True) + def cleanup(self, client): + yield + + for rec in self._new_records: + client.delete_record_by_id(app_id=rec["app_id"], record_id=rec["record_id"]) + + self._new_records.clear() + + @pytest.mark.flaky(reruns=3) + def test_add(self, client, test_survey_id, test_text_field): + record = Record( + app_id=test_survey_id, + fields=[StringFieldValue(field_id=test_text_field, value="Test")], + ) + + response = client.add_or_update_record(record) + + assert response.status_code == 201 + assert response.is_successful + assert response.message is not None + assert response.data is not None + assert response.data.id is not None + assert response.data.warnings is not None + + self._new_records.append({"app_id": test_survey_id, "record_id": response.data.id}) + + @pytest.mark.flaky(reruns=3) + async def test_add_async(self, async_client, test_survey_id, test_text_field): + record = Record( + app_id=test_survey_id, + fields=[StringFieldValue(field_id=test_text_field, value="Test")], + ) + + response = await async_client.add_or_update_record(record) + + assert response.status_code == 201 + assert response.is_successful + assert response.data is not None + assert response.data.id is not None + self._new_records.append({"app_id": test_survey_id, "record_id": response.data.id}) + + if response.data.id: + import os + + from onspring_api_sdk import OnspringClient + + sync = OnspringClient(os.environ["API_BASE_URL"], os.environ["SANDBOX_API_KEY"]) + sync.delete_record_by_id(app_id=test_survey_id, record_id=response.data.id) + + @pytest.mark.flaky(reruns=3) + def test_update(self, client, test_survey_id, test_text_field): + new_record = Record( + app_id=test_survey_id, + fields=[StringFieldValue(field_id=test_text_field, value="Test")], + ) + + new_response = client.add_or_update_record(new_record) + + assert new_response.data is not None + + new_id = new_response.data.id + + self._new_records.append({"app_id": test_survey_id, "record_id": new_id}) + + update_record = Record( + app_id=test_survey_id, + record_id=new_id, + fields=[StringFieldValue(field_id=test_text_field, value="updated")], + ) + + response = client.add_or_update_record(update_record) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.id == new_id + assert response.data.warnings is not None + + @pytest.mark.flaky(reruns=3) + def test_empty_fields_returns_400(self, client, test_survey_id): + record = Record(app_id=test_survey_id, fields=[]) + + response = client.add_or_update_record(record) + + assert response.status_code == 400 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + record = Record(app_id=0, fields=[]) + + response = bad.add_or_update_record(record) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_app_id_no_access): + record = Record(app_id=test_app_id_no_access, fields=[]) + + response = client.add_or_update_record(record) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_record_not_found_returns_404(self, client, test_survey_id): + record = Record(app_id=test_survey_id, record_id=0, fields=[]) + + response = client.add_or_update_record(record) + + assert response.status_code == 404 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + +class TestDeleteRecordById: + def _create_temp_record(self, client, test_survey_id, test_text_field): + record = Record( + app_id=test_survey_id, + fields=[StringFieldValue(field_id=test_text_field, value="to_delete")], + ) + + response = client.add_or_update_record(record) + + assert response.data is not None + return response.data.id + + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_survey_id, test_text_field): + record_id = self._create_temp_record(client, test_survey_id, test_text_field) + + response = client.delete_record_by_id(app_id=test_survey_id, record_id=record_id) + + assert response.status_code == 204 + assert response.is_successful + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_survey_id, test_text_field): + import os + + from onspring_api_sdk import OnspringClient + + sync = OnspringClient(os.environ["API_BASE_URL"], os.environ["SANDBOX_API_KEY"]) + + record_id = self._create_temp_record(sync, test_survey_id, test_text_field) + + response = await async_client.delete_record_by_id(app_id=test_survey_id, record_id=record_id) + + assert response.status_code == 204 + assert response.is_successful + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + response = bad.delete_record_by_id(app_id=1, record_id=1) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_app_id_no_access): + response = client.delete_record_by_id(app_id=test_app_id_no_access, record_id=1) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_record_not_found_returns_404(self, client, test_app_id): + response = client.delete_record_by_id(app_id=test_app_id, record_id=0) + + assert response.status_code == 404 + assert not response.is_successful + assert response.data is None + + +class TestDeleteRecordsByIds: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_survey_id, test_text_field): + r1 = client.add_or_update_record( + Record(app_id=test_survey_id, fields=[StringFieldValue(field_id=test_text_field, value="del1")]) + ) + + r2 = client.add_or_update_record( + Record(app_id=test_survey_id, fields=[StringFieldValue(field_id=test_text_field, value="del2")]) + ) + + assert r1.data is not None and r2.data is not None + + request = DeleteBatchRecordsRequest(app_id=test_survey_id, record_ids=[r1.data.id, r2.data.id]) + + response = client.delete_records_by_ids(request) + + assert response.status_code == 204 + assert response.is_successful + + @pytest.mark.flaky(reruns=3) + def test_empty_ids_returns_400(self, client, test_survey_id): + request = DeleteBatchRecordsRequest(app_id=test_survey_id, record_ids=[]) + + response = client.delete_records_by_ids(request) + + assert response.status_code == 400 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + request = DeleteBatchRecordsRequest(app_id=1, record_ids=[1]) + + response = bad.delete_records_by_ids(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_app_id_no_access): + request = DeleteBatchRecordsRequest(app_id=test_app_id_no_access, record_ids=[1]) + + response = client.delete_records_by_ids(request) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + +class TestGetRecordsByIds: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_survey_id, test_survey_record_id): + request = GetBatchRecordsRequest(app_id=test_survey_id, record_ids=[test_survey_record_id]) + + response = client.get_records_by_ids(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.count is not None + assert len(response.data.records) > 0 + + for record in response.data.records: + assert record.app_id == test_survey_id + assert record.record_id is not None + assert record.fields is not None + assert len(record.fields) > 0 + + for field in record.fields: + assert field.field_id is not None + assert field.type is not None + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_survey_id, test_survey_record_id): + request = GetBatchRecordsRequest(app_id=test_survey_id, record_ids=[test_survey_record_id]) + + response = await async_client.get_records_by_ids(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert len(response.data.records) > 0 + + @pytest.mark.flaky(reruns=3) + def test_with_params(self, client, test_survey_id, test_survey_record_id, test_text_field): + request = GetBatchRecordsRequest( + app_id=test_survey_id, + record_ids=[test_survey_record_id], + field_ids=[test_text_field], + data_format=DataFormat.Formatted.name, + ) + + response = client.get_records_by_ids(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert len(response.data.records) > 0 + + @pytest.mark.flaky(reruns=3) + async def test_with_params_async(self, async_client, test_survey_id, test_survey_record_id, test_text_field): + request = GetBatchRecordsRequest( + app_id=test_survey_id, + record_ids=[test_survey_record_id], + field_ids=[test_text_field], + data_format=DataFormat.Formatted.name, + ) + + response = await async_client.get_records_by_ids(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + + @pytest.mark.flaky(reruns=3) + def test_too_many_ids_returns_400(self, client): + record_ids = list(range(1, 102)) + request = GetBatchRecordsRequest(app_id=1, record_ids=record_ids) + + response = client.get_records_by_ids(request) + + assert response.status_code == 400 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_too_many_ids_returns_400_async(self, async_client): + record_ids = list(range(1, 102)) + request = GetBatchRecordsRequest(app_id=1, record_ids=record_ids) + + response = await async_client.get_records_by_ids(request) + + assert response.status_code == 400 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + request = GetBatchRecordsRequest(app_id=1, record_ids=[1]) + + response = bad.get_records_by_ids(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async(self, base_url): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + request = GetBatchRecordsRequest(app_id=1, record_ids=[1]) + + response = await bad.get_records_by_ids(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_app_id_no_access): + request = GetBatchRecordsRequest(app_id=test_app_id_no_access, record_ids=[1]) + + response = client.get_records_by_ids(request) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_access_returns_403_async(self, async_client, test_app_id_no_access): + request = GetBatchRecordsRequest(app_id=test_app_id_no_access, record_ids=[1]) + + response = await async_client.get_records_by_ids(request) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None + + +class TestQueryRecords: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_survey_id, test_survey_auto_number_field): + filter_str = f"{test_survey_auto_number_field} gt 0" + request = QueryRecordsRequest(app_id=test_survey_id, filter=filter_str) + + response = client.query_records(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.page_number is not None + assert response.data.page_size is not None + assert response.data.total_pages is not None + assert response.data.total_records is not None + + for record in response.data.records: + assert record.app_id == test_survey_id + assert record.record_id is not None + assert record.fields is not None + + if record.fields: + for field in record.fields: + assert field.field_id is not None + assert field.type is not None + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_survey_id, test_survey_auto_number_field): + filter_str = f"{test_survey_auto_number_field} gt 0" + request = QueryRecordsRequest(app_id=test_survey_id, filter=filter_str) + + response = await async_client.query_records(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.total_records is not None + + @pytest.mark.flaky(reruns=3) + def test_with_params(self, client, test_survey_id, test_survey_auto_number_field): + filter_str = f"{test_survey_auto_number_field} gt 0" + request = QueryRecordsRequest( + app_id=test_survey_id, + filter=filter_str, + field_ids=[test_survey_auto_number_field], + data_format=DataFormat.Formatted.name, + page_number=1, + page_size=1, + ) + + response = client.query_records(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.page_number == 1 + assert response.data.page_size == 1 + + @pytest.mark.flaky(reruns=3) + async def test_with_params_async(self, async_client, test_survey_id, test_survey_auto_number_field): + filter_str = f"{test_survey_auto_number_field} gt 0" + request = QueryRecordsRequest( + app_id=test_survey_id, + filter=filter_str, + field_ids=[test_survey_auto_number_field], + data_format=DataFormat.Formatted.name, + page_number=1, + page_size=1, + ) + + response = await async_client.query_records(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.page_number == 1 + assert response.data.page_size == 1 + + @pytest.mark.flaky(reruns=3) + def test_invalid_page_size_returns_400(self, client, test_survey_auto_number_field): + filter_str = f"{test_survey_auto_number_field} gt 0" + request = QueryRecordsRequest( + app_id=1, + filter=filter_str, + page_size=-1, + ) + + response = client.query_records(request) + + assert response.status_code == 400 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_page_size_returns_400_async(self, async_client, test_survey_auto_number_field): + filter_str = f"{test_survey_auto_number_field} gt 0" + request = QueryRecordsRequest( + app_id=1, + filter=filter_str, + page_size=-1, + ) + + response = await async_client.query_records(request) + + assert response.status_code == 400 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + request = QueryRecordsRequest(app_id=1, filter="") + + response = bad.query_records(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async(self, base_url): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + request = QueryRecordsRequest(app_id=1, filter="") + + response = await bad.query_records(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_app_id_no_access, test_text_field): + filter_str = f"{test_text_field} gt ''" + request = QueryRecordsRequest(app_id=test_app_id_no_access, filter=filter_str) + + response = client.query_records(request) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_access_returns_403_async(self, async_client, test_app_id_no_access, test_text_field): + filter_str = f"{test_text_field} gt ''" + request = QueryRecordsRequest(app_id=test_app_id_no_access, filter=filter_str) + + response = await async_client.query_records(request) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None diff --git a/tests/integration/test_reports.py b/tests/integration/test_reports.py new file mode 100644 index 0000000..3b131e3 --- /dev/null +++ b/tests/integration/test_reports.py @@ -0,0 +1,278 @@ +import pytest + +from onspring_api_sdk.enums import ReportDataType +from onspring_api_sdk.models import GetReportByIdRequest, PagingRequest + +pytestmark = pytest.mark.integration + + +class TestGetReportById: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_report): + request = GetReportByIdRequest(report_id=test_report) + + response = client.get_report_by_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.columns is not None + assert len(response.data.columns) > 0 + assert response.data.rows is not None + assert len(response.data.rows) > 0 + + for row in response.data.rows: + assert row.record_id is not None + assert row.cells is not None + assert len(row.cells) > 0 + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_report): + request = GetReportByIdRequest(report_id=test_report) + + response = await async_client.get_report_by_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert len(response.data.rows) > 0 + + @pytest.mark.flaky(reruns=3) + def test_with_chart_data_report(self, client, test_report_with_chart_data): + request = GetReportByIdRequest(report_id=test_report_with_chart_data) + + response = client.get_report_by_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.columns is not None + assert response.data.rows is not None + + @pytest.mark.flaky(reruns=3) + async def test_with_chart_data_report_async(self, async_client, test_report_with_chart_data): + request = GetReportByIdRequest(report_id=test_report_with_chart_data) + + response = await async_client.get_report_by_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + + @pytest.mark.flaky(reruns=3) + def test_request_chart_data(self, client, test_report_with_chart_data): + request = GetReportByIdRequest( + report_id=test_report_with_chart_data, + data_type=ReportDataType.ChartData.name, + ) + + response = client.get_report_by_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert response.data.columns is not None + assert response.data.rows is not None + + @pytest.mark.flaky(reruns=3) + async def test_request_chart_data_async(self, async_client, test_report_with_chart_data): + request = GetReportByIdRequest( + report_id=test_report_with_chart_data, + data_type=ReportDataType.ChartData.name, + ) + + response = await async_client.get_report_by_id(request) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + + @pytest.mark.flaky(reruns=3) + def test_chart_data_on_report_without_returns_400(self, client, test_report): + request = GetReportByIdRequest( + report_id=test_report, + data_type=ReportDataType.ChartData.name, + ) + + response = client.get_report_by_id(request) + + assert response.status_code == 400 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_chart_data_on_report_without_returns_400_async(self, async_client, test_report): + request = GetReportByIdRequest( + report_id=test_report, + data_type=ReportDataType.ChartData.name, + ) + + response = await async_client.get_report_by_id(request) + + assert response.status_code == 400 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + request = GetReportByIdRequest(report_id=1) + + response = bad.get_report_by_id(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async(self, base_url): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + request = GetReportByIdRequest(report_id=1) + + response = await bad.get_report_by_id(request) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client): + request = GetReportByIdRequest(report_id=1) + + response = client.get_report_by_id(request) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_access_returns_403_async(self, async_client): + request = GetReportByIdRequest(report_id=1) + + response = await async_client.get_report_by_id(request) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_not_found_returns_404(self, client): + request = GetReportByIdRequest(report_id=0) + + response = client.get_report_by_id(request) + + assert response.status_code == 404 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_not_found_returns_404_async(self, async_client): + request = GetReportByIdRequest(report_id=0) + + response = await async_client.get_report_by_id(request) + + assert response.status_code == 404 + assert not response.is_successful + assert response.data is None + + +class TestGetReportsByAppId: + @pytest.mark.flaky(reruns=3) + def test_sync(self, client, test_survey_id): + response = client.get_reports_by_app_id(app_id=test_survey_id) + + assert response.status_code == 200 + assert response.is_successful + assert response.message is None + assert response.data is not None + assert response.data.page_number is not None + assert response.data.page_size is not None + assert response.data.total_pages is not None + assert response.data.total_records is not None + assert response.data.reports is not None + assert len(response.data.reports) > 0 + + for report in response.data.reports: + assert report.app_id is not None + assert report.id is not None + assert report.name is not None + + @pytest.mark.flaky(reruns=3) + async def test_async(self, async_client, test_survey_id): + response = await async_client.get_reports_by_app_id(app_id=test_survey_id) + + assert response.status_code == 200 + assert response.is_successful + assert response.data is not None + assert len(response.data.reports) > 0 + + @pytest.mark.flaky(reruns=3) + def test_invalid_page_size_returns_400(self, client, test_survey_id): + response = client.get_reports_by_app_id( + app_id=test_survey_id, + paging_request=PagingRequest(page_number=1, page_size=-1), + ) + + assert response.status_code == 400 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_page_size_returns_400_async(self, async_client, test_survey_id): + response = await async_client.get_reports_by_app_id( + app_id=test_survey_id, + paging_request=PagingRequest(page_number=1, page_size=-1), + ) + + assert response.status_code == 400 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_invalid_api_key_returns_401(self, base_url): + from onspring_api_sdk import OnspringClient + + bad = OnspringClient(base_url, "invalid") + response = bad.get_reports_by_app_id(app_id=1) + + assert response.status_code == 401 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_invalid_api_key_returns_401_async(self, base_url): + from onspring_api_sdk import AsyncOnspringClient + + bad = AsyncOnspringClient(base_url, "invalid") + response = await bad.get_reports_by_app_id(app_id=1) + + assert response.status_code == 401 + assert not response.is_successful + assert response.data is None + + @pytest.mark.flaky(reruns=3) + def test_no_access_returns_403(self, client, test_app_id_no_access): + response = client.get_reports_by_app_id(app_id=test_app_id_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.message is not None + assert response.data is None + + @pytest.mark.flaky(reruns=3) + async def test_no_access_returns_403_async(self, async_client, test_app_id_no_access): + response = await async_client.get_reports_by_app_id(app_id=test_app_id_no_access) + + assert response.status_code == 403 + assert not response.is_successful + assert response.data is None diff --git a/tests/integration/testdata/test-attachment.txt b/tests/integration/testdata/test-attachment.txt new file mode 100644 index 0000000..3eae1d0 --- /dev/null +++ b/tests/integration/testdata/test-attachment.txt @@ -0,0 +1 @@ +This is a test attachment. \ No newline at end of file diff --git a/tests/integration/testdata/test-image.jpeg b/tests/integration/testdata/test-image.jpeg new file mode 100644 index 0000000..b493033 Binary files /dev/null and b/tests/integration/testdata/test-image.jpeg differ diff --git a/tests/integration/utils/__init__.py b/tests/integration/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/utils/add_record.py b/tests/integration/utils/add_record.py new file mode 100644 index 0000000..19b9e9a --- /dev/null +++ b/tests/integration/utils/add_record.py @@ -0,0 +1,33 @@ +import os + +import pytest + +from onspring_api_sdk import OnspringClient +from onspring_api_sdk.models import Record, StringFieldValue + + +def add_record(base_url: str, api_key: str) -> int: + client = OnspringClient(base_url, api_key) + + survey_id = os.environ.get("TEST_SURVEY_ID") + text_field = os.environ.get("TEST_TEXT_FIELD") + + if survey_id is None: + pytest.fail("TEST_SURVEY_ID is not defined") + if text_field is None: + pytest.fail("TEST_TEXT_FIELD is not defined") + + app_id = int(survey_id) + field_id = int(text_field) + + record = Record( + app_id=app_id, + fields=[StringFieldValue(field_id=field_id, value="test")], + ) + + response = client.add_or_update_record(record) + + if response.data is None or response.data.id is None: + pytest.fail("Record ID is not defined") + + return response.data.id diff --git a/tests/test_async_client.py b/tests/test_async_client.py new file mode 100644 index 0000000..5767563 --- /dev/null +++ b/tests/test_async_client.py @@ -0,0 +1,1344 @@ +import pytest +import respx +from httpx import Response + +from onspring_api_sdk import AsyncOnspringClient +from onspring_api_sdk.errors import ( + OnspringAuthenticationError, + OnspringError, + OnspringNotFoundError, + OnspringRateLimitError, +) +from onspring_api_sdk.models import ( + AddOrUpdateListItemResponse, + AddOrUpdateRecordResponse, + ApiResponse, + GetAppByIdResponse, + GetAppsByIdsResponse, + GetAppsResponse, + GetBatchRecordsResponse, + GetFieldByIdResponse, + GetFieldsByAppIdResponse, + GetFieldsByIdsResponse, + GetFileByIdResponse, + GetFileInfoByIdResponse, + GetRecordByIdRequest, + GetRecordsByAppRequest, + GetRecordsResponse, + GetReportByIdRequest, + GetReportByIdResponse, + GetReportsByAppIdResponse, + Record, + SaveFileRequest, + SaveFileResponse, +) + +from .conftest import ( + MOCK_APP, + MOCK_APPS_BATCH_RESPONSE, + MOCK_APPS_RESPONSE, + MOCK_FIELD, + MOCK_FIELDS_BATCH_RESPONSE, + MOCK_FIELDS_RESPONSE, + MOCK_FILE_INFO, + MOCK_LIST_ITEM_RESPONSE, + MOCK_MESSAGE_RESPONSE, + MOCK_RECORD, + MOCK_RECORDS_BATCH_RESPONSE, + MOCK_RECORDS_RESPONSE, + MOCK_REPORT_RESPONSE, + MOCK_REPORTS_BY_APP_RESPONSE, + MOCK_SAVE_FILE_RESPONSE, + MOCK_SAVE_RECORD_RESPONSE, + TEST_URL, + create_temp_file, +) + + +def _mock_json(status: int, json: dict | list | None = None) -> Response: + return Response(status, json=json) + + +def _assert_error(response: ApiResponse, status: int, message: str | None) -> None: + assert response.status_code == status + assert response.is_successful is False + assert response.data is None + assert response.message == message + + +class TestCanConnect: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Ping").mock(return_value=Response(200)) + + assert await async_client.can_connect() is True + + async def test_failure(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Ping").mock(return_value=Response(401)) + + assert await async_client.can_connect() is False + + async def test_async_context_manager(self, async_client: AsyncOnspringClient): + async with async_client as cm: + assert cm is async_client + + assert async_client.client.is_closed + + async def test_aclose(self, async_client: AsyncOnspringClient): + await async_client.aclose() + assert async_client.client.is_closed + + +class TestGetApps: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps").mock(return_value=Response(200, json=MOCK_APPS_RESPONSE)) + + response = await async_client.get_apps() + + assert response.is_successful + assert isinstance(response.data, GetAppsResponse) + assert len(response.data.apps) == 2 + assert response.data.apps[0].name == "Test App" + + async def test_400(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps").mock(return_value=Response(400)) + + _assert_error(await async_client.get_apps(), 400, "Invalid paging information") + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps").mock(return_value=Response(401)) + + _assert_error(await async_client.get_apps(), 401, "Unauthorized request") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps").mock(return_value=Response(418)) + + _assert_error(await async_client.get_apps(), 418, None) + + async def test_with_explicit_paging(self, async_client: AsyncOnspringClient): + from onspring_api_sdk.models import PagingRequest + + with respx.mock: + respx.get(f"{TEST_URL}/Apps").mock(return_value=Response(200, json=MOCK_APPS_RESPONSE)) + + paging = PagingRequest(page_number=2, page_size=10) + response = await async_client.get_apps(paging_request=paging) + + assert response.is_successful + assert isinstance(response.data, GetAppsResponse) + + +class TestGetAppById: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps/id/1").mock(return_value=Response(200, json=MOCK_APP)) + + response = await async_client.get_app_by_id(1) + + assert response.is_successful + assert isinstance(response.data, GetAppByIdResponse) + assert response.data.app.id == 1 + assert response.data.app.name == "Test App" + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps/id/1").mock(return_value=Response(401)) + + _assert_error(await async_client.get_app_by_id(1), 401, "Unauthorized request") + + async def test_403(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps/id/1").mock(return_value=Response(403)) + + _assert_error(await async_client.get_app_by_id(1), 403, "Client does not have read access to the app") + + async def test_404(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps/id/999").mock(return_value=Response(404)) + + _assert_error(await async_client.get_app_by_id(999), 404, "App could not be found") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps/id/1").mock(return_value=Response(418)) + + _assert_error(await async_client.get_app_by_id(1), 418, None) + + +class TestGetAppsByIds: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Apps/batch-get").mock(return_value=Response(200, json=MOCK_APPS_BATCH_RESPONSE)) + + response = await async_client.get_apps_by_ids([1, 2]) + + assert response.is_successful + assert isinstance(response.data, GetAppsByIdsResponse) + assert response.data.count == 2 + + async def test_type_check(self, async_client: AsyncOnspringClient): + response = await async_client.get_apps_by_ids("not a list") + + _assert_error(response, 400, "App ids should be of type list or tuple") + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Apps/batch-get").mock(return_value=Response(401)) + + _assert_error(await async_client.get_apps_by_ids([1]), 401, "Unauthorized request") + + async def test_403(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Apps/batch-get").mock(return_value=Response(403)) + + _assert_error(await async_client.get_apps_by_ids([1]), 403, "Client does not have read access to the app") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Apps/batch-get").mock(return_value=Response(418)) + + _assert_error(await async_client.get_apps_by_ids([1]), 418, None) + + +class TestGetFieldById: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/id/1").mock(return_value=Response(200, json=MOCK_FIELD)) + + response = await async_client.get_field_by_id(1) + + assert response.is_successful + assert isinstance(response.data, GetFieldByIdResponse) + assert response.data.field.id == 1 + assert response.data.field.name == "Test Field" + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/id/1").mock(return_value=Response(401)) + + _assert_error(await async_client.get_field_by_id(1), 401, "Unauthorized request") + + async def test_403(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/id/1").mock(return_value=Response(403)) + + _assert_error(await async_client.get_field_by_id(1), 403, "Client does not have read access to the field") + + async def test_404(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/id/999").mock(return_value=Response(404)) + + _assert_error(await async_client.get_field_by_id(999), 404, "Field could not be found") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/id/1").mock(return_value=Response(418)) + + _assert_error(await async_client.get_field_by_id(1), 418, None) + + +class TestGetFieldsByIds: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Fields/batch-get").mock(return_value=Response(200, json=MOCK_FIELDS_BATCH_RESPONSE)) + + response = await async_client.get_fields_by_ids([1, 2]) + + assert response.is_successful + assert isinstance(response.data, GetFieldsByIdsResponse) + assert response.data.count == 2 + + async def test_type_check(self, async_client: AsyncOnspringClient): + response = await async_client.get_fields_by_ids("not a list") + + _assert_error(response, 400, "Field ids should be of type list or tuple") + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Fields/batch-get").mock(return_value=Response(401)) + + _assert_error(await async_client.get_fields_by_ids([1]), 401, "Unauthorized request") + + async def test_403(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Fields/batch-get").mock(return_value=Response(403)) + + _assert_error( + await async_client.get_fields_by_ids([1]), 403, "Client does not have read access to the field(s)" + ) + + async def test_404(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Fields/batch-get").mock(return_value=Response(404)) + + _assert_error(await async_client.get_fields_by_ids([1]), 404, "Field(s) could not be found") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Fields/batch-get").mock(return_value=Response(418)) + + _assert_error(await async_client.get_fields_by_ids([1]), 418, None) + + +class TestGetFieldsByAppId: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/appId/1").mock(return_value=Response(200, json=MOCK_FIELDS_RESPONSE)) + + response = await async_client.get_fields_by_app_id(1) + + assert response.is_successful + assert isinstance(response.data, GetFieldsByAppIdResponse) + assert len(response.data.fields) == 2 + assert response.data.fields[0].id == 1 + + async def test_400(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/appId/1").mock(return_value=Response(400)) + + _assert_error(await async_client.get_fields_by_app_id(1), 400, "Invalid paging information") + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/appId/1").mock(return_value=Response(401)) + + _assert_error(await async_client.get_fields_by_app_id(1), 401, "Unauthorized request") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/appId/1").mock(return_value=Response(418)) + + _assert_error(await async_client.get_fields_by_app_id(1), 418, None) + + async def test_with_explicit_paging(self, async_client: AsyncOnspringClient): + from onspring_api_sdk.models import PagingRequest + + with respx.mock: + respx.get(f"{TEST_URL}/Fields/appId/1").mock(return_value=Response(200, json=MOCK_FIELDS_RESPONSE)) + + paging = PagingRequest(page_number=2, page_size=10) + response = await async_client.get_fields_by_app_id(1, paging_request=paging) + + assert response.is_successful + assert isinstance(response.data, GetFieldsByAppIdResponse) + + +class TestGetFileInfoById: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock( + return_value=Response(200, json=MOCK_FILE_INFO) + ) + + response = await async_client.get_file_info_by_id(1, 2, 3) + + assert response.is_successful + assert isinstance(response.data, GetFileInfoByIdResponse) + assert response.data.file_info.name == "test.txt" + assert response.data.file_info.content_type == "text/plain" + + async def test_400(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(400)) + + _assert_error( + await async_client.get_file_info_by_id(1, 2, 3), 400, "Request is invalid based on underlying data" + ) + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(401)) + + _assert_error(await async_client.get_file_info_by_id(1, 2, 3), 401, "Unauthorized request") + + async def test_403(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(403)) + + _assert_error( + await async_client.get_file_info_by_id(1, 2, 3), 403, "Client does not have read access to the file" + ) + + async def test_404(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(404)) + + _assert_error(await async_client.get_file_info_by_id(1, 2, 3), 404, "File could not be found") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(418)) + + _assert_error(await async_client.get_file_info_by_id(1, 2, 3), 418, None) + + +class TestDeleteFileById: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(204)) + + response = await async_client.delete_file_by_id(1, 2, 3) + + assert response.is_successful + assert response.status_code == 204 + assert response.message == "File deleted successfully" + + async def test_400(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(400)) + + _assert_error( + await async_client.delete_file_by_id(1, 2, 3), 400, "Request is invalid based on underlying data" + ) + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(401)) + + _assert_error(await async_client.delete_file_by_id(1, 2, 3), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock( + return_value=Response(403, json=MOCK_MESSAGE_RESPONSE) + ) + + response = await async_client.delete_file_by_id(1, 2, 3) + + _assert_error(response, 403, "An error occurred") + + async def test_404_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock( + return_value=Response(404, json=MOCK_MESSAGE_RESPONSE) + ) + + response = await async_client.delete_file_by_id(1, 2, 3) + + _assert_error(response, 404, "An error occurred") + + async def test_500(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(500)) + + _assert_error( + await async_client.delete_file_by_id(1, 2, 3), 500, "File could not be deleted due to internal error" + ) + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(418)) + + _assert_error(await async_client.delete_file_by_id(1, 2, 3), 418, None) + + +class TestGetFileById: + def _mock_file_response(self, headers: dict | None = None) -> Response: + h = { + "content-disposition": "filename=test.txt", + "content-type": "text/plain", + "content-length": "12", + } + + if headers: + h.update(headers) + + return Response(200, headers=h, content=b"Hello World!") + + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock( + return_value=self._mock_file_response() + ) + + response = await async_client.get_file_by_id(1, 2, 3) + + assert response.is_successful + assert isinstance(response.data, GetFileByIdResponse) + assert response.data.file.name == "test.txt" + assert response.data.file.content_type == "text/plain" + assert response.data.file.content_length == 12 + assert response.data.file.content == b"Hello World!" + + async def test_quoted_filename(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock( + return_value=self._mock_file_response({"content-disposition": 'attachment; filename="quoted.pdf"'}) + ) + + response = await async_client.get_file_by_id(1, 2, 3) + + assert response.data.file.name == "quoted.pdf" + + async def test_success_onspring_fallback(self, async_client: AsyncOnspringClient): + """When no content-disposition header, fall back to 'OnspringFile'.""" + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock( + return_value=self._mock_file_response({"content-disposition": ""}) + ) + + response = await async_client.get_file_by_id(1, 2, 3) + + assert response.data.file.name == "OnspringFile" + + async def test_400(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock(return_value=Response(400)) + + _assert_error( + await async_client.get_file_by_id(1, 2, 3), 400, "Request is invalid based on underlying data" + ) + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock(return_value=Response(401)) + + _assert_error(await async_client.get_file_by_id(1, 2, 3), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock( + return_value=Response(403, json=MOCK_MESSAGE_RESPONSE) + ) + + response = await async_client.get_file_by_id(1, 2, 3) + + _assert_error(response, 403, "An error occurred") + + async def test_404_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock( + return_value=Response(404, json=MOCK_MESSAGE_RESPONSE) + ) + + response = await async_client.get_file_by_id(1, 2, 3) + + _assert_error(response, 404, "An error occurred") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock(return_value=Response(418)) + + _assert_error(await async_client.get_file_by_id(1, 2, 3), 418, None) + + +class TestSaveFile: + async def test_success(self, async_client: AsyncOnspringClient): + file_path = create_temp_file() + request = SaveFileRequest( + recordId=1, + fieldId=2, + fileName="test.txt", + filePath=str(file_path), + contentType="text/plain", + ) + + with respx.mock: + respx.post(f"{TEST_URL}/Files").mock(return_value=Response(201, json=MOCK_SAVE_FILE_RESPONSE)) + + response = await async_client.save_file(request) + + assert response.is_successful + assert response.status_code == 201 + assert isinstance(response.data, SaveFileResponse) + assert response.data.id == 1 + + async def test_400(self, async_client: AsyncOnspringClient): + file_path = create_temp_file() + request = SaveFileRequest( + recordId=1, + fieldId=2, + fileName="test.txt", + filePath=str(file_path), + contentType="text/plain", + ) + + with respx.mock: + respx.post(f"{TEST_URL}/Files").mock(return_value=Response(400)) + + _assert_error(await async_client.save_file(request), 400, "Request is invalid based on underlying data") + + async def test_401(self, async_client: AsyncOnspringClient): + file_path = create_temp_file() + request = SaveFileRequest( + recordId=1, + fieldId=2, + fileName="test.txt", + filePath=str(file_path), + contentType="text/plain", + ) + + with respx.mock: + respx.post(f"{TEST_URL}/Files").mock(return_value=Response(401)) + + _assert_error(await async_client.save_file(request), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + file_path = create_temp_file() + request = SaveFileRequest( + recordId=1, + fieldId=2, + fileName="test.txt", + filePath=str(file_path), + contentType="text/plain", + ) + + with respx.mock: + respx.post(f"{TEST_URL}/Files").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + response = await async_client.save_file(request) + + _assert_error(response, 403, "An error occurred") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + file_path = create_temp_file() + request = SaveFileRequest( + recordId=1, + fieldId=2, + fileName="test.txt", + filePath=str(file_path), + contentType="text/plain", + ) + + with respx.mock: + respx.post(f"{TEST_URL}/Files").mock(return_value=Response(418)) + + _assert_error(await async_client.save_file(request), 418, None) + + +class TestAddOrUpdateListItem: + async def test_200_update(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Lists/id/100/items").mock(return_value=Response(200, json=MOCK_LIST_ITEM_RESPONSE)) + from onspring_api_sdk.models import ListItemRequest + + request = ListItemRequest(listId=100, name="Updated Item") + response = await async_client.add_or_update_list_item(request) + + assert response.is_successful + assert response.status_code == 200 + assert response.message == "Existing list value successfully updated" + assert isinstance(response.data, AddOrUpdateListItemResponse) + + async def test_201_create(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Lists/id/100/items").mock(return_value=Response(201, json=MOCK_LIST_ITEM_RESPONSE)) + + from onspring_api_sdk.models import ListItemRequest + + request = ListItemRequest(listId=100, name="New Item") + response = await async_client.add_or_update_list_item(request) + + assert response.is_successful + assert response.status_code == 201 + assert response.message == "New list value successfully added" + assert isinstance(response.data, AddOrUpdateListItemResponse) + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Lists/id/100/items").mock(return_value=Response(401)) + + from onspring_api_sdk.models import ListItemRequest + + request = ListItemRequest(listId=100, name="Item") + + _assert_error(await async_client.add_or_update_list_item(request), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Lists/id/100/items").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + from onspring_api_sdk.models import ListItemRequest + + request = ListItemRequest(listId=100, name="Item") + response = await async_client.add_or_update_list_item(request) + + _assert_error(response, 403, "An error occurred") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Lists/id/100/items").mock(return_value=Response(418)) + + from onspring_api_sdk.models import ListItemRequest + + request = ListItemRequest(listId=100, name="Item") + + _assert_error(await async_client.add_or_update_list_item(request), 418, None) + + +class TestDeleteListItem: + async def test_success(self, async_client: AsyncOnspringClient): + item_id = "2c1af5b1-0f90-4378-b9a5-8b7e22f2bc84" + + with respx.mock: + respx.delete(f"{TEST_URL}/Lists/id/100/itemId/{item_id}").mock(return_value=Response(204)) + + response = await async_client.delete_list_item(100, item_id) + + assert response.is_successful + assert response.status_code == 204 + assert response.message == "Item deleted successfully" + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Lists/id/100/itemId/test").mock(return_value=Response(401)) + + _assert_error(await async_client.delete_list_item(100, "test"), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Lists/id/100/itemId/test").mock( + return_value=Response(403, json=MOCK_MESSAGE_RESPONSE) + ) + + response = await async_client.delete_list_item(100, "test") + + _assert_error(response, 403, "An error occurred") + + async def test_404(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Lists/id/100/itemId/test").mock(return_value=Response(404)) + + _assert_error(await async_client.delete_list_item(100, "test"), 404, "List/item could not be found") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Lists/id/100/itemId/test").mock(return_value=Response(418)) + + _assert_error(await async_client.delete_list_item(100, "test"), 418, None) + + +class TestGetRecordsByAppRequestDefaults: + async def test_default_page_values(self): + from onspring_api_sdk.models import GetRecordsByAppRequest + + request = GetRecordsByAppRequest(app_id=100) + + assert request.page_number == 1 + assert request.page_size == 50 + + +class TestGetRecordsByAppId: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100").mock(return_value=Response(200, json=MOCK_RECORDS_RESPONSE)) + + request = GetRecordsByAppRequest(app_id=100) + + response = await async_client.get_records_by_app_id(request) + + assert response.is_successful + assert isinstance(response.data, GetRecordsResponse) + assert len(response.data.records) == 1 + assert response.data.records[0].record_id == 1 + + async def test_400(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100").mock(return_value=Response(400)) + + request = GetRecordsByAppRequest(app_id=100) + + _assert_error( + await async_client.get_records_by_app_id(request), + 400, + "Invalid paging information/size of the data requested was too large.", + ) + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100").mock(return_value=Response(401)) + + request = GetRecordsByAppRequest(app_id=100) + + _assert_error(await async_client.get_records_by_app_id(request), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + request = GetRecordsByAppRequest(app_id=100) + + response = await async_client.get_records_by_app_id(request) + + _assert_error(response, 403, "An error occurred") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100").mock(return_value=Response(418)) + + request = GetRecordsByAppRequest(app_id=100) + + _assert_error(await async_client.get_records_by_app_id(request), 418, None) + + +class TestGetRecordById: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100/recordId/1").mock(return_value=Response(200, json=MOCK_RECORD)) + + request = GetRecordByIdRequest(app_id=100, record_id=1) + + response = await async_client.get_record_by_id(request) + + assert response.is_successful + assert isinstance(response.data, Record) + assert response.data.record_id == 1 + assert len(response.data.fields) == 2 + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100/recordId/1").mock(return_value=Response(401)) + + request = GetRecordByIdRequest(app_id=100, record_id=1) + + _assert_error(await async_client.get_record_by_id(request), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100/recordId/1").mock( + return_value=Response(403, json=MOCK_MESSAGE_RESPONSE) + ) + + request = GetRecordByIdRequest(app_id=100, record_id=1) + + response = await async_client.get_record_by_id(request) + + _assert_error(response, 403, "An error occurred") + + async def test_404(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100/recordId/999").mock(return_value=Response(404)) + + request = GetRecordByIdRequest(app_id=100, record_id=999) + + _assert_error(await async_client.get_record_by_id(request), 404, "Record could not be found") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100/recordId/1").mock(return_value=Response(418)) + + request = GetRecordByIdRequest(app_id=100, record_id=1) + + _assert_error(await async_client.get_record_by_id(request), 418, None) + + +class TestDeleteRecordById: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Records/appId/100/recordId/1").mock(return_value=Response(204)) + + response = await async_client.delete_record_by_id(100, 1) + + assert response.is_successful + assert response.status_code == 204 + assert response.message == "Record deleted successfully" + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Records/appId/100/recordId/1").mock(return_value=Response(401)) + + _assert_error(await async_client.delete_record_by_id(100, 1), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Records/appId/100/recordId/1").mock( + return_value=Response(403, json=MOCK_MESSAGE_RESPONSE) + ) + + response = await async_client.delete_record_by_id(100, 1) + + _assert_error(response, 403, "An error occurred") + + async def test_404(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Records/appId/100/recordId/999").mock(return_value=Response(404)) + + _assert_error(await async_client.delete_record_by_id(100, 999), 404, "Record could not be found") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Records/appId/100/recordId/1").mock(return_value=Response(418)) + + _assert_error(await async_client.delete_record_by_id(100, 1), 418, None) + + +class TestGetRecordsByIds: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-get").mock( + return_value=Response(200, json=MOCK_RECORDS_BATCH_RESPONSE) + ) + + from onspring_api_sdk.models import GetBatchRecordsRequest + + request = GetBatchRecordsRequest(app_id=100, recordIds=[1]) + + response = await async_client.get_records_by_ids(request) + + assert response.is_successful + assert isinstance(response.data, GetBatchRecordsResponse) + assert response.data.count == 1 + + async def test_400(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-get").mock(return_value=Response(400)) + + from onspring_api_sdk.models import GetBatchRecordsRequest + + request = GetBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error( + await async_client.get_records_by_ids(request), + 400, + "Batch request is invalid/size of the data requested was too large.", + ) + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-get").mock(return_value=Response(401)) + + from onspring_api_sdk.models import GetBatchRecordsRequest + + request = GetBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error(await async_client.get_records_by_ids(request), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-get").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + from onspring_api_sdk.models import GetBatchRecordsRequest + + request = GetBatchRecordsRequest(app_id=100, recordIds=[1]) + + response = await async_client.get_records_by_ids(request) + + _assert_error(response, 403, "An error occurred") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-get").mock(return_value=Response(418)) + + from onspring_api_sdk.models import GetBatchRecordsRequest + + request = GetBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error(await async_client.get_records_by_ids(request), 418, None) + + +class TestQueryRecordsRequestDefaults: + async def test_default_page_values(self): + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="test") + + assert request.page_number == 1 + assert request.page_size == 50 + + +class TestQueryRecords: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/Query").mock(return_value=Response(200, json=MOCK_RECORDS_RESPONSE)) + + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="Test") + + response = await async_client.query_records(request) + + assert response.is_successful + assert isinstance(response.data, GetRecordsResponse) + assert len(response.data.records) == 1 + + async def test_400(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/Query").mock(return_value=Response(400)) + + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="Test") + + _assert_error( + await async_client.query_records(request), + 400, + "Query request is invalid/size of the data requested was too large.", + ) + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/Query").mock(return_value=Response(401)) + + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="Test") + + _assert_error(await async_client.query_records(request), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/Query").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="Test") + + response = await async_client.query_records(request) + + _assert_error(response, 403, "An error occurred") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/Query").mock(return_value=Response(418)) + + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="Test") + + _assert_error(await async_client.query_records(request), 418, None) + + async def test_request_body_excludes_page_params(self, async_client: AsyncOnspringClient): + with respx.mock: + route = respx.post(f"{TEST_URL}/Records/Query").mock(return_value=Response(200, json=MOCK_RECORDS_RESPONSE)) + + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="Test", page_number=2, page_size=10) + await async_client.query_records(request) + + body = route.calls[0].request.content + assert b"pageNumber" not in body + assert b"pageSize" not in body + + +class TestAddOrUpdateRecord: + def _make_record(self) -> Record: + from onspring_api_sdk.models import StringFieldValue + + return Record( + appId=100, + fieldData=[StringFieldValue(fieldId=1, value="test")], + ) + + async def test_200_update(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(200, json=MOCK_SAVE_RECORD_RESPONSE)) + + response = await async_client.add_or_update_record(self._make_record()) + + assert response.is_successful + assert response.status_code == 200 + assert response.message == "Record updated successfully" + assert isinstance(response.data, AddOrUpdateRecordResponse) + assert response.data.id == 1 + + async def test_201_create(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(201, json=MOCK_SAVE_RECORD_RESPONSE)) + + response = await async_client.add_or_update_record(self._make_record()) + + assert response.is_successful + assert response.status_code == 201 + assert response.message == "Record created successfully" + assert isinstance(response.data, AddOrUpdateRecordResponse) + assert response.data.id == 1 + + async def test_400(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(400)) + + _assert_error(await async_client.add_or_update_record(self._make_record()), 400, "Request data is invalid") + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(401)) + + _assert_error(await async_client.add_or_update_record(self._make_record()), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + response = await async_client.add_or_update_record(self._make_record()) + + _assert_error(response, 403, "An error occurred") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(418)) + + _assert_error(await async_client.add_or_update_record(self._make_record()), 418, None) + + async def test_with_guid_field(self, async_client: AsyncOnspringClient): + import uuid + + from onspring_api_sdk.models import GuidFieldValue, Record + + record = Record( + appId=100, + fieldData=[GuidFieldValue(fieldId=1, value=uuid.UUID("12345678-1234-5678-1234-567812345678"))], + ) + + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(200, json=MOCK_SAVE_RECORD_RESPONSE)) + response = await async_client.add_or_update_record(record) + + assert response.is_successful + + async def test_payload_excludes_field_data(self, async_client: AsyncOnspringClient): + from onspring_api_sdk.models import Record, StringFieldValue + + record = Record( + appId=100, + fieldData=[StringFieldValue(fieldId=1, value="test")], + ) + + with respx.mock: + route = respx.put(f"{TEST_URL}/Records").mock(return_value=Response(200, json=MOCK_SAVE_RECORD_RESPONSE)) + await async_client.add_or_update_record(record) + + body = route.calls[0].request.content + assert b"fieldData" not in body + + +class TestDeleteRecordsByIds: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-delete").mock(return_value=Response(204)) + + from onspring_api_sdk.models import DeleteBatchRecordsRequest + + request = DeleteBatchRecordsRequest(app_id=100, recordIds=[1, 2]) + + response = await async_client.delete_records_by_ids(request) + + assert response.is_successful + assert response.status_code == 204 + assert response.message == "Record(s) deleted successfully" + + async def test_400(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-delete").mock(return_value=Response(400)) + + from onspring_api_sdk.models import DeleteBatchRecordsRequest + + request = DeleteBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error(await async_client.delete_records_by_ids(request), 400, "Invalid request provided") + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-delete").mock(return_value=Response(401)) + + from onspring_api_sdk.models import DeleteBatchRecordsRequest + + request = DeleteBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error(await async_client.delete_records_by_ids(request), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-delete").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + from onspring_api_sdk.models import DeleteBatchRecordsRequest + + request = DeleteBatchRecordsRequest(app_id=100, recordIds=[1]) + + response = await async_client.delete_records_by_ids(request) + + _assert_error(response, 403, "An error occurred") + + async def test_404(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-delete").mock(return_value=Response(404)) + + from onspring_api_sdk.models import DeleteBatchRecordsRequest + + request = DeleteBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error(await async_client.delete_records_by_ids(request), 404, "Records could not be found") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-delete").mock(return_value=Response(418)) + + from onspring_api_sdk.models import DeleteBatchRecordsRequest + + request = DeleteBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error(await async_client.delete_records_by_ids(request), 418, None) + + +class TestGetReportById: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(200, json=MOCK_REPORT_RESPONSE)) + + request = GetReportByIdRequest(report_id=53) + + response = await async_client.get_report_by_id(request) + + assert response.is_successful + assert isinstance(response.data, GetReportByIdResponse) + assert len(response.data.columns) == 2 + assert len(response.data.rows) == 1 + assert response.data.rows[0].record_id == 1 + + async def test_400(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(400)) + + request = GetReportByIdRequest(report_id=53) + + _assert_error(await async_client.get_report_by_id(request), 400, "Invalid request based on underlying data") + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(401)) + + request = GetReportByIdRequest(report_id=53) + + _assert_error(await async_client.get_report_by_id(request), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + request = GetReportByIdRequest(report_id=53) + + response = await async_client.get_report_by_id(request) + + _assert_error(response, 403, "An error occurred") + + async def test_404(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/999").mock(return_value=Response(404)) + + request = GetReportByIdRequest(report_id=999) + + _assert_error(await async_client.get_report_by_id(request), 404, "Report could not be found") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(418)) + + request = GetReportByIdRequest(report_id=53) + + _assert_error(await async_client.get_report_by_id(request), 418, None) + + async def test_params_excludes_report_id(self, async_client: AsyncOnspringClient): + request = GetReportByIdRequest(report_id=53) + + with respx.mock: + route = respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(200, json=MOCK_REPORT_RESPONSE)) + await async_client.get_report_by_id(request) + + assert "reportId" not in route.calls[0].request.url.params + + async def test_403_empty_body(self, async_client: AsyncOnspringClient): + request = GetReportByIdRequest(report_id=53) + + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(403, content=b"")) + response = await async_client.get_report_by_id(request) + + assert response.status_code == 403 + assert response.is_successful is False + + +class TestGetReportsByAppId: + async def test_success(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/appId/10").mock( + return_value=Response(200, json=MOCK_REPORTS_BY_APP_RESPONSE) + ) + + response = await async_client.get_reports_by_app_id(10) + + assert response.is_successful + assert isinstance(response.data, GetReportsByAppIdResponse) + assert len(response.data.reports) == 1 + assert response.data.reports[0].id == 53 + + async def test_400(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/appId/10").mock(return_value=Response(400)) + + _assert_error( + await async_client.get_reports_by_app_id(10), 400, "Client does not have read access to the app." + ) + + async def test_401(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/appId/10").mock(return_value=Response(401)) + + _assert_error(await async_client.get_reports_by_app_id(10), 401, "Unauthorized request") + + async def test_403_with_message(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/appId/10").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + response = await async_client.get_reports_by_app_id(10) + + _assert_error(response, 403, "An error occurred") + + async def test_fallthrough(self, async_client: AsyncOnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/appId/10").mock(return_value=Response(418)) + + _assert_error(await async_client.get_reports_by_app_id(10), 418, None) + + async def test_with_explicit_paging(self, async_client: AsyncOnspringClient): + from onspring_api_sdk.models import PagingRequest + + with respx.mock: + respx.get(f"{TEST_URL}/Reports/appId/10").mock( + return_value=Response(200, json=MOCK_REPORTS_BY_APP_RESPONSE) + ) + + paging = PagingRequest(page_number=2, page_size=10) + response = await async_client.get_reports_by_app_id(10, paging_request=paging) + + assert response.is_successful + assert isinstance(response.data, GetReportsByAppIdResponse) + + +class TestRaiseForStatus: + async def test_401_raises_authentication_error(self): + response = ApiResponse(status_code=401, message="Unauthorized") + + with pytest.raises(OnspringAuthenticationError, match="Unauthorized"): + response.raise_for_status() + + async def test_403_raises_authentication_error(self): + response = ApiResponse(status_code=403, message="Forbidden") + + with pytest.raises(OnspringAuthenticationError, match="Forbidden"): + response.raise_for_status() + + async def test_404_raises_not_found_error(self): + response = ApiResponse(status_code=404, message="Not Found") + + with pytest.raises(OnspringNotFoundError, match="Not Found"): + response.raise_for_status() + + async def test_429_raises_rate_limit_error(self): + response = ApiResponse(status_code=429, message="Rate limited") + + with pytest.raises(OnspringRateLimitError, match="Rate limited"): + response.raise_for_status() + + async def test_418_raises_generic_error(self): + response = ApiResponse(status_code=418, message="Teapot") + + with pytest.raises(OnspringError, match="Teapot"): + response.raise_for_status() + + async def test_success_does_not_raise(self): + response = ApiResponse(status_code=200, data="ok") + response.raise_for_status() diff --git a/tests/test_sync_client.py b/tests/test_sync_client.py new file mode 100644 index 0000000..0829774 --- /dev/null +++ b/tests/test_sync_client.py @@ -0,0 +1,1352 @@ +import pytest +import respx +from httpx import Response + +from onspring_api_sdk import OnspringClient +from onspring_api_sdk.errors import ( + OnspringAuthenticationError, + OnspringError, + OnspringNotFoundError, + OnspringRateLimitError, +) +from onspring_api_sdk.models import ( + AddOrUpdateListItemResponse, + AddOrUpdateRecordResponse, + ApiResponse, + GetAppByIdResponse, + GetAppsByIdsResponse, + GetAppsResponse, + GetBatchRecordsResponse, + GetFieldByIdResponse, + GetFieldsByAppIdResponse, + GetFieldsByIdsResponse, + GetFileByIdResponse, + GetFileInfoByIdResponse, + GetRecordByIdRequest, + GetRecordsByAppRequest, + GetRecordsResponse, + GetReportByIdRequest, + GetReportByIdResponse, + GetReportsByAppIdResponse, + Record, + SaveFileRequest, + SaveFileResponse, +) + +from .conftest import ( + MOCK_APP, + MOCK_APPS_BATCH_RESPONSE, + MOCK_APPS_RESPONSE, + MOCK_FIELD, + MOCK_FIELDS_BATCH_RESPONSE, + MOCK_FIELDS_RESPONSE, + MOCK_FILE_INFO, + MOCK_LIST_FIELD, + MOCK_LIST_ITEM_RESPONSE, + MOCK_MESSAGE_RESPONSE, + MOCK_RECORD, + MOCK_RECORDS_BATCH_RESPONSE, + MOCK_RECORDS_RESPONSE, + MOCK_REPORT_RESPONSE, + MOCK_REPORTS_BY_APP_RESPONSE, + MOCK_SAVE_FILE_RESPONSE, + MOCK_SAVE_RECORD_RESPONSE, + TEST_URL, + create_temp_file, +) + + +def _mock_json(status: int, json: dict | list | None = None) -> Response: + return Response(status, json=json) + + +def _assert_error(response: ApiResponse, status: int, message: str | None) -> None: + assert response.status_code == status + assert response.is_successful is False + assert response.data is None + assert response.message == message + + +class TestCanConnect: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Ping").mock(return_value=Response(200)) + + assert client.can_connect() is True + + def test_failure(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Ping").mock(return_value=Response(401)) + + assert client.can_connect() is False + + def test_context_manager(self, client: OnspringClient): + with client as cm: + assert cm is client + + assert client.client.is_closed + + def test_close(self, client: OnspringClient): + client.close() + assert client.client.is_closed + + +class TestGetApps: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps").mock(return_value=Response(200, json=MOCK_APPS_RESPONSE)) + + response = client.get_apps() + + assert response.is_successful + assert isinstance(response.data, GetAppsResponse) + assert len(response.data.apps) == 2 + assert response.data.apps[0].name == "Test App" + + def test_400(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps").mock(return_value=Response(400)) + + _assert_error(client.get_apps(), 400, "Invalid paging information") + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps").mock(return_value=Response(401)) + + _assert_error(client.get_apps(), 401, "Unauthorized request") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps").mock(return_value=Response(418)) + + _assert_error(client.get_apps(), 418, None) + + def test_with_explicit_paging(self, client: OnspringClient): + from onspring_api_sdk.models import PagingRequest + + with respx.mock: + respx.get(f"{TEST_URL}/Apps").mock(return_value=Response(200, json=MOCK_APPS_RESPONSE)) + + paging = PagingRequest(page_number=2, page_size=10) + response = client.get_apps(paging_request=paging) + + assert response.is_successful + assert isinstance(response.data, GetAppsResponse) + + +class TestGetAppById: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps/id/1").mock(return_value=Response(200, json=MOCK_APP)) + + response = client.get_app_by_id(1) + + assert response.is_successful + assert isinstance(response.data, GetAppByIdResponse) + assert response.data.app.id == 1 + assert response.data.app.name == "Test App" + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps/id/1").mock(return_value=Response(401)) + + _assert_error(client.get_app_by_id(1), 401, "Unauthorized request") + + def test_403(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps/id/1").mock(return_value=Response(403)) + + _assert_error(client.get_app_by_id(1), 403, "Client does not have read access to the app") + + def test_404(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps/id/999").mock(return_value=Response(404)) + + _assert_error(client.get_app_by_id(999), 404, "App could not be found") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Apps/id/1").mock(return_value=Response(418)) + + _assert_error(client.get_app_by_id(1), 418, None) + + +class TestGetAppsByIds: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Apps/batch-get").mock(return_value=Response(200, json=MOCK_APPS_BATCH_RESPONSE)) + + response = client.get_apps_by_ids([1, 2]) + + assert response.is_successful + assert isinstance(response.data, GetAppsByIdsResponse) + assert response.data.count == 2 + + def test_type_check(self, client: OnspringClient): + response = client.get_apps_by_ids("not a list") + + _assert_error(response, 400, "App ids should be of type list or tuple") + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Apps/batch-get").mock(return_value=Response(401)) + + _assert_error(client.get_apps_by_ids([1]), 401, "Unauthorized request") + + def test_403(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Apps/batch-get").mock(return_value=Response(403)) + + _assert_error(client.get_apps_by_ids([1]), 403, "Client does not have read access to the app") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Apps/batch-get").mock(return_value=Response(418)) + + _assert_error(client.get_apps_by_ids([1]), 418, None) + + +class TestGetFieldById: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/id/1").mock(return_value=Response(200, json=MOCK_FIELD)) + + response = client.get_field_by_id(1) + + assert response.is_successful + assert isinstance(response.data, GetFieldByIdResponse) + assert response.data.field.id == 1 + assert response.data.field.name == "Test Field" + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/id/1").mock(return_value=Response(401)) + + _assert_error(client.get_field_by_id(1), 401, "Unauthorized request") + + def test_403(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/id/1").mock(return_value=Response(403)) + + _assert_error(client.get_field_by_id(1), 403, "Client does not have read access to the field") + + def test_404(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/id/999").mock(return_value=Response(404)) + + _assert_error(client.get_field_by_id(999), 404, "Field could not be found") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/id/1").mock(return_value=Response(418)) + + _assert_error(client.get_field_by_id(1), 418, None) + + def test_list_field_values(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/id/2").mock(return_value=Response(200, json=MOCK_LIST_FIELD)) + + response = client.get_field_by_id(2) + + assert response.data.field.values is not None + assert response.data.field.values[0].name == "list_value_1" + assert response.data.field.values[0].numeric_value == 1 + assert response.data.field.values[0].color == "#008e8e" + + +class TestGetFieldsByIds: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Fields/batch-get").mock(return_value=Response(200, json=MOCK_FIELDS_BATCH_RESPONSE)) + + response = client.get_fields_by_ids([1, 2]) + + assert response.is_successful + assert isinstance(response.data, GetFieldsByIdsResponse) + assert response.data.count == 2 + + def test_type_check(self, client: OnspringClient): + response = client.get_fields_by_ids("not a list") + + _assert_error(response, 400, "Field ids should be of type list or tuple") + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Fields/batch-get").mock(return_value=Response(401)) + + _assert_error(client.get_fields_by_ids([1]), 401, "Unauthorized request") + + def test_403(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Fields/batch-get").mock(return_value=Response(403)) + + _assert_error(client.get_fields_by_ids([1]), 403, "Client does not have read access to the field(s)") + + def test_404(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Fields/batch-get").mock(return_value=Response(404)) + + _assert_error(client.get_fields_by_ids([1]), 404, "Field(s) could not be found") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Fields/batch-get").mock(return_value=Response(418)) + + _assert_error(client.get_fields_by_ids([1]), 418, None) + + +class TestGetFieldsByAppId: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/appId/1").mock(return_value=Response(200, json=MOCK_FIELDS_RESPONSE)) + + response = client.get_fields_by_app_id(1) + + assert response.is_successful + assert isinstance(response.data, GetFieldsByAppIdResponse) + assert len(response.data.fields) == 2 + assert response.data.fields[0].id == 1 + + def test_400(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/appId/1").mock(return_value=Response(400)) + + _assert_error(client.get_fields_by_app_id(1), 400, "Invalid paging information") + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/appId/1").mock(return_value=Response(401)) + + _assert_error(client.get_fields_by_app_id(1), 401, "Unauthorized request") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Fields/appId/1").mock(return_value=Response(418)) + + _assert_error(client.get_fields_by_app_id(1), 418, None) + + def test_with_explicit_paging(self, client: OnspringClient): + from onspring_api_sdk.models import PagingRequest + + with respx.mock: + respx.get(f"{TEST_URL}/Fields/appId/1").mock(return_value=Response(200, json=MOCK_FIELDS_RESPONSE)) + + paging = PagingRequest(page_number=2, page_size=10) + response = client.get_fields_by_app_id(1, paging_request=paging) + + assert response.is_successful + assert isinstance(response.data, GetFieldsByAppIdResponse) + + +class TestGetFileInfoById: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock( + return_value=Response(200, json=MOCK_FILE_INFO) + ) + + response = client.get_file_info_by_id(1, 2, 3) + + assert response.is_successful + assert isinstance(response.data, GetFileInfoByIdResponse) + assert response.data.file_info.name == "test.txt" + assert response.data.file_info.content_type == "text/plain" + + def test_400(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(400)) + + _assert_error(client.get_file_info_by_id(1, 2, 3), 400, "Request is invalid based on underlying data") + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(401)) + + _assert_error(client.get_file_info_by_id(1, 2, 3), 401, "Unauthorized request") + + def test_403(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(403)) + + _assert_error(client.get_file_info_by_id(1, 2, 3), 403, "Client does not have read access to the file") + + def test_404(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(404)) + + _assert_error(client.get_file_info_by_id(1, 2, 3), 404, "File could not be found") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(418)) + + _assert_error(client.get_file_info_by_id(1, 2, 3), 418, None) + + +class TestDeleteFileById: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(204)) + + response = client.delete_file_by_id(1, 2, 3) + + assert response.is_successful + assert response.status_code == 204 + assert response.message == "File deleted successfully" + + def test_400(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(400)) + + _assert_error(client.delete_file_by_id(1, 2, 3), 400, "Request is invalid based on underlying data") + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(401)) + + _assert_error(client.delete_file_by_id(1, 2, 3), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock( + return_value=Response(403, json=MOCK_MESSAGE_RESPONSE) + ) + + response = client.delete_file_by_id(1, 2, 3) + + _assert_error(response, 403, "An error occurred") + + def test_404_with_message(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock( + return_value=Response(404, json=MOCK_MESSAGE_RESPONSE) + ) + + response = client.delete_file_by_id(1, 2, 3) + + _assert_error(response, 404, "An error occurred") + + def test_500(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(500)) + + _assert_error(client.delete_file_by_id(1, 2, 3), 500, "File could not be deleted due to internal error") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3").mock(return_value=Response(418)) + + _assert_error(client.delete_file_by_id(1, 2, 3), 418, None) + + +class TestGetFileById: + def _mock_file_response(self, headers: dict | None = None) -> Response: + h = { + "content-disposition": "filename=test.txt", + "content-type": "text/plain", + "content-length": "12", + } + + if headers: + h.update(headers) + + return Response(200, headers=h, content=b"Hello World!") + + def test_success(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock( + return_value=self._mock_file_response() + ) + + response = client.get_file_by_id(1, 2, 3) + + assert response.is_successful + assert isinstance(response.data, GetFileByIdResponse) + assert response.data.file.name == "test.txt" + assert response.data.file.content_type == "text/plain" + assert response.data.file.content_length == 12 + assert response.data.file.content == b"Hello World!" + + def test_quoted_filename(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock( + return_value=self._mock_file_response({"content-disposition": 'attachment; filename="quoted.pdf"'}) + ) + + response = client.get_file_by_id(1, 2, 3) + + assert response.data.file.name == "quoted.pdf" + + def test_success_onspring_fallback(self, client: OnspringClient): + """When no content-disposition header, fall back to 'OnspringFile'.""" + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock( + return_value=self._mock_file_response({"content-disposition": ""}) + ) + + response = client.get_file_by_id(1, 2, 3) + + assert response.data.file.name == "OnspringFile" + + def test_400(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock(return_value=Response(400)) + + _assert_error(client.get_file_by_id(1, 2, 3), 400, "Request is invalid based on underlying data") + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock(return_value=Response(401)) + + _assert_error(client.get_file_by_id(1, 2, 3), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock( + return_value=Response(403, json=MOCK_MESSAGE_RESPONSE) + ) + + response = client.get_file_by_id(1, 2, 3) + + _assert_error(response, 403, "An error occurred") + + def test_404_with_message(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock( + return_value=Response(404, json=MOCK_MESSAGE_RESPONSE) + ) + + response = client.get_file_by_id(1, 2, 3) + + _assert_error(response, 404, "An error occurred") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Files/recordId/1/fieldId/2/fileId/3/file").mock(return_value=Response(418)) + + _assert_error(client.get_file_by_id(1, 2, 3), 418, None) + + +class TestSaveFile: + def test_success(self, client: OnspringClient): + file_path = create_temp_file() + request = SaveFileRequest( + recordId=1, + fieldId=2, + fileName="test.txt", + filePath=str(file_path), + contentType="text/plain", + ) + + with respx.mock: + respx.post(f"{TEST_URL}/Files").mock(return_value=Response(201, json=MOCK_SAVE_FILE_RESPONSE)) + + response = client.save_file(request) + + assert response.is_successful + assert response.status_code == 201 + assert isinstance(response.data, SaveFileResponse) + assert response.data.id == 1 + + def test_400(self, client: OnspringClient): + file_path = create_temp_file() + request = SaveFileRequest( + recordId=1, + fieldId=2, + fileName="test.txt", + filePath=str(file_path), + contentType="text/plain", + ) + + with respx.mock: + respx.post(f"{TEST_URL}/Files").mock(return_value=Response(400)) + + _assert_error(client.save_file(request), 400, "Request is invalid based on underlying data") + + def test_401(self, client: OnspringClient): + file_path = create_temp_file() + request = SaveFileRequest( + recordId=1, + fieldId=2, + fileName="test.txt", + filePath=str(file_path), + contentType="text/plain", + ) + + with respx.mock: + respx.post(f"{TEST_URL}/Files").mock(return_value=Response(401)) + + _assert_error(client.save_file(request), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + file_path = create_temp_file() + request = SaveFileRequest( + recordId=1, + fieldId=2, + fileName="test.txt", + filePath=str(file_path), + contentType="text/plain", + ) + + with respx.mock: + respx.post(f"{TEST_URL}/Files").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + response = client.save_file(request) + + _assert_error(response, 403, "An error occurred") + + def test_fallthrough(self, client: OnspringClient): + file_path = create_temp_file() + request = SaveFileRequest( + recordId=1, + fieldId=2, + fileName="test.txt", + filePath=str(file_path), + contentType="text/plain", + ) + + with respx.mock: + respx.post(f"{TEST_URL}/Files").mock(return_value=Response(418)) + + _assert_error(client.save_file(request), 418, None) + + +class TestAddOrUpdateListItem: + def test_200_update(self, client: OnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Lists/id/100/items").mock(return_value=Response(200, json=MOCK_LIST_ITEM_RESPONSE)) + + from onspring_api_sdk.models import ListItemRequest + + request = ListItemRequest(listId=100, name="Updated Item") + + response = client.add_or_update_list_item(request) + + assert response.is_successful + assert response.status_code == 200 + assert response.message == "Existing list value successfully updated" + assert isinstance(response.data, AddOrUpdateListItemResponse) + + def test_201_create(self, client: OnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Lists/id/100/items").mock(return_value=Response(201, json=MOCK_LIST_ITEM_RESPONSE)) + + from onspring_api_sdk.models import ListItemRequest + + request = ListItemRequest(listId=100, name="New Item") + + response = client.add_or_update_list_item(request) + + assert response.is_successful + assert response.status_code == 201 + assert response.message == "New list value successfully added" + assert isinstance(response.data, AddOrUpdateListItemResponse) + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Lists/id/100/items").mock(return_value=Response(401)) + + from onspring_api_sdk.models import ListItemRequest + + request = ListItemRequest(listId=100, name="Item") + + _assert_error(client.add_or_update_list_item(request), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Lists/id/100/items").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + from onspring_api_sdk.models import ListItemRequest + + request = ListItemRequest(listId=100, name="Item") + + response = client.add_or_update_list_item(request) + + _assert_error(response, 403, "An error occurred") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Lists/id/100/items").mock(return_value=Response(418)) + + from onspring_api_sdk.models import ListItemRequest + + request = ListItemRequest(listId=100, name="Item") + + _assert_error(client.add_or_update_list_item(request), 418, None) + + +class TestDeleteListItem: + def test_success(self, client: OnspringClient): + item_id = "2c1af5b1-0f90-4378-b9a5-8b7e22f2bc84" + + with respx.mock: + respx.delete(f"{TEST_URL}/Lists/id/100/itemId/{item_id}").mock(return_value=Response(204)) + + response = client.delete_list_item(100, item_id) + + assert response.is_successful + assert response.status_code == 204 + assert response.message == "Item deleted successfully" + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Lists/id/100/itemId/test").mock(return_value=Response(401)) + + _assert_error(client.delete_list_item(100, "test"), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Lists/id/100/itemId/test").mock( + return_value=Response(403, json=MOCK_MESSAGE_RESPONSE) + ) + + response = client.delete_list_item(100, "test") + + _assert_error(response, 403, "An error occurred") + + def test_404(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Lists/id/100/itemId/test").mock(return_value=Response(404)) + + _assert_error(client.delete_list_item(100, "test"), 404, "List/item could not be found") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Lists/id/100/itemId/test").mock(return_value=Response(418)) + + _assert_error(client.delete_list_item(100, "test"), 418, None) + + def test_endpoint_accepts_string(self): + from onspring_api_sdk.endpoints import delete_list_item_endpoint + + url = delete_list_item_endpoint("https://test.com", 1, "my-string-id") + assert url == "https://test.com/Lists/id/1/itemId/my-string-id" + + +class TestGetRecordsByAppRequestDefaults: + def test_default_page_values(self): + from onspring_api_sdk.models import GetRecordsByAppRequest + + request = GetRecordsByAppRequest(app_id=100) + + assert request.page_number == 1 + assert request.page_size == 50 + + +class TestGetRecordsByAppId: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100").mock(return_value=Response(200, json=MOCK_RECORDS_RESPONSE)) + + request = GetRecordsByAppRequest(app_id=100) + + response = client.get_records_by_app_id(request) + + assert response.is_successful + assert isinstance(response.data, GetRecordsResponse) + assert len(response.data.records) == 1 + assert response.data.records[0].record_id == 1 + + def test_400(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100").mock(return_value=Response(400)) + + request = GetRecordsByAppRequest(app_id=100) + + _assert_error( + client.get_records_by_app_id(request), + 400, + "Invalid paging information/size of the data requested was too large.", + ) + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100").mock(return_value=Response(401)) + + request = GetRecordsByAppRequest(app_id=100) + + _assert_error(client.get_records_by_app_id(request), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + request = GetRecordsByAppRequest(app_id=100) + + response = client.get_records_by_app_id(request) + + _assert_error(response, 403, "An error occurred") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100").mock(return_value=Response(418)) + + request = GetRecordsByAppRequest(app_id=100) + + _assert_error(client.get_records_by_app_id(request), 418, None) + + +class TestGetRecordById: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100/recordId/1").mock(return_value=Response(200, json=MOCK_RECORD)) + + request = GetRecordByIdRequest(app_id=100, record_id=1) + + response = client.get_record_by_id(request) + + assert response.is_successful + assert isinstance(response.data, Record) + assert response.data.record_id == 1 + assert len(response.data.fields) == 2 + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100/recordId/1").mock(return_value=Response(401)) + + request = GetRecordByIdRequest(app_id=100, record_id=1) + + _assert_error(client.get_record_by_id(request), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100/recordId/1").mock( + return_value=Response(403, json=MOCK_MESSAGE_RESPONSE) + ) + + request = GetRecordByIdRequest(app_id=100, record_id=1) + + response = client.get_record_by_id(request) + + _assert_error(response, 403, "An error occurred") + + def test_404(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100/recordId/999").mock(return_value=Response(404)) + + request = GetRecordByIdRequest(app_id=100, record_id=999) + + _assert_error(client.get_record_by_id(request), 404, "Record could not be found") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Records/appId/100/recordId/1").mock(return_value=Response(418)) + + request = GetRecordByIdRequest(app_id=100, record_id=1) + + _assert_error(client.get_record_by_id(request), 418, None) + + +class TestDeleteRecordById: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Records/appId/100/recordId/1").mock(return_value=Response(204)) + + response = client.delete_record_by_id(100, 1) + + assert response.is_successful + assert response.status_code == 204 + assert response.message == "Record deleted successfully" + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Records/appId/100/recordId/1").mock(return_value=Response(401)) + + _assert_error(client.delete_record_by_id(100, 1), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Records/appId/100/recordId/1").mock( + return_value=Response(403, json=MOCK_MESSAGE_RESPONSE) + ) + + response = client.delete_record_by_id(100, 1) + + _assert_error(response, 403, "An error occurred") + + def test_404(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Records/appId/100/recordId/999").mock(return_value=Response(404)) + + _assert_error(client.delete_record_by_id(100, 999), 404, "Record could not be found") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.delete(f"{TEST_URL}/Records/appId/100/recordId/1").mock(return_value=Response(418)) + + _assert_error(client.delete_record_by_id(100, 1), 418, None) + + +class TestGetRecordsByIds: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-get").mock( + return_value=Response(200, json=MOCK_RECORDS_BATCH_RESPONSE) + ) + + from onspring_api_sdk.models import GetBatchRecordsRequest + + request = GetBatchRecordsRequest(app_id=100, recordIds=[1]) + + response = client.get_records_by_ids(request) + + assert response.is_successful + assert isinstance(response.data, GetBatchRecordsResponse) + assert response.data.count == 1 + + def test_400(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-get").mock(return_value=Response(400)) + + from onspring_api_sdk.models import GetBatchRecordsRequest + + request = GetBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error( + client.get_records_by_ids(request), + 400, + "Batch request is invalid/size of the data requested was too large.", + ) + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-get").mock(return_value=Response(401)) + + from onspring_api_sdk.models import GetBatchRecordsRequest + + request = GetBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error(client.get_records_by_ids(request), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-get").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + from onspring_api_sdk.models import GetBatchRecordsRequest + + request = GetBatchRecordsRequest(app_id=100, recordIds=[1]) + + response = client.get_records_by_ids(request) + + _assert_error(response, 403, "An error occurred") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-get").mock(return_value=Response(418)) + + from onspring_api_sdk.models import GetBatchRecordsRequest + + request = GetBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error(client.get_records_by_ids(request), 418, None) + + +class TestQueryRecordsRequestDefaults: + def test_default_page_values(self): + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="test") + + assert request.page_number == 1 + assert request.page_size == 50 + + +class TestQueryRecords: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/Query").mock(return_value=Response(200, json=MOCK_RECORDS_RESPONSE)) + + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="Test") + + response = client.query_records(request) + + assert response.is_successful + assert isinstance(response.data, GetRecordsResponse) + assert len(response.data.records) == 1 + + def test_400(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/Query").mock(return_value=Response(400)) + + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="Test") + + _assert_error( + client.query_records(request), 400, "Query request is invalid/size of the data requested was too large." + ) + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/Query").mock(return_value=Response(401)) + + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="Test") + + _assert_error(client.query_records(request), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/Query").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="Test") + + response = client.query_records(request) + + _assert_error(response, 403, "An error occurred") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/Query").mock(return_value=Response(418)) + + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="Test") + + _assert_error(client.query_records(request), 418, None) + + def test_request_body_excludes_page_params(self, client: OnspringClient): + with respx.mock: + route = respx.post(f"{TEST_URL}/Records/Query").mock(return_value=Response(200, json=MOCK_RECORDS_RESPONSE)) + + from onspring_api_sdk.models import QueryRecordsRequest + + request = QueryRecordsRequest(app_id=100, filter="Test", page_number=2, page_size=10) + client.query_records(request) + + body = route.calls[0].request.content + assert b"pageNumber" not in body + assert b"pageSize" not in body + + +class TestAddOrUpdateRecord: + def _make_record(self) -> Record: + from onspring_api_sdk.models import StringFieldValue + + return Record( + appId=100, + fieldData=[StringFieldValue(fieldId=1, value="test")], + ) + + def test_200_update(self, client: OnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(200, json=MOCK_SAVE_RECORD_RESPONSE)) + + response = client.add_or_update_record(self._make_record()) + + assert response.is_successful + assert response.status_code == 200 + assert response.message == "Record updated successfully" + assert isinstance(response.data, AddOrUpdateRecordResponse) + assert response.data.id == 1 + + def test_201_create(self, client: OnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(201, json=MOCK_SAVE_RECORD_RESPONSE)) + + response = client.add_or_update_record(self._make_record()) + + assert response.is_successful + assert response.status_code == 201 + assert response.message == "Record created successfully" + assert isinstance(response.data, AddOrUpdateRecordResponse) + assert response.data.id == 1 + + def test_400(self, client: OnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(400)) + + _assert_error(client.add_or_update_record(self._make_record()), 400, "Request data is invalid") + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(401)) + + _assert_error(client.add_or_update_record(self._make_record()), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + response = client.add_or_update_record(self._make_record()) + + _assert_error(response, 403, "An error occurred") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(418)) + + _assert_error(client.add_or_update_record(self._make_record()), 418, None) + + def test_with_guid_field(self, client: OnspringClient): + import uuid + + from onspring_api_sdk.models import GuidFieldValue, Record + + record = Record( + appId=100, + fieldData=[GuidFieldValue(fieldId=1, value=uuid.UUID("12345678-1234-5678-1234-567812345678"))], + ) + + with respx.mock: + respx.put(f"{TEST_URL}/Records").mock(return_value=Response(200, json=MOCK_SAVE_RECORD_RESPONSE)) + response = client.add_or_update_record(record) + + assert response.is_successful + + def test_payload_excludes_field_data(self, client: OnspringClient): + from onspring_api_sdk.models import Record, StringFieldValue + + record = Record( + appId=100, + fieldData=[StringFieldValue(fieldId=1, value="test")], + ) + + with respx.mock: + route = respx.put(f"{TEST_URL}/Records").mock(return_value=Response(200, json=MOCK_SAVE_RECORD_RESPONSE)) + client.add_or_update_record(record) + + body = route.calls[0].request.content + assert b"fieldData" not in body + + +class TestDeleteRecordsByIds: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-delete").mock(return_value=Response(204)) + + from onspring_api_sdk.models import DeleteBatchRecordsRequest + + request = DeleteBatchRecordsRequest(app_id=100, recordIds=[1, 2]) + + response = client.delete_records_by_ids(request) + + assert response.is_successful + assert response.status_code == 204 + assert response.message == "Record(s) deleted successfully" + + def test_400(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-delete").mock(return_value=Response(400)) + + from onspring_api_sdk.models import DeleteBatchRecordsRequest + + request = DeleteBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error(client.delete_records_by_ids(request), 400, "Invalid request provided") + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-delete").mock(return_value=Response(401)) + + from onspring_api_sdk.models import DeleteBatchRecordsRequest + + request = DeleteBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error(client.delete_records_by_ids(request), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-delete").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + from onspring_api_sdk.models import DeleteBatchRecordsRequest + + request = DeleteBatchRecordsRequest(app_id=100, recordIds=[1]) + + response = client.delete_records_by_ids(request) + + _assert_error(response, 403, "An error occurred") + + def test_404(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-delete").mock(return_value=Response(404)) + + from onspring_api_sdk.models import DeleteBatchRecordsRequest + + request = DeleteBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error(client.delete_records_by_ids(request), 404, "Records could not be found") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.post(f"{TEST_URL}/Records/batch-delete").mock(return_value=Response(418)) + + from onspring_api_sdk.models import DeleteBatchRecordsRequest + + request = DeleteBatchRecordsRequest(app_id=100, recordIds=[1]) + + _assert_error(client.delete_records_by_ids(request), 418, None) + + +class TestGetReportById: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(200, json=MOCK_REPORT_RESPONSE)) + + request = GetReportByIdRequest(report_id=53) + + response = client.get_report_by_id(request) + + assert response.is_successful + assert isinstance(response.data, GetReportByIdResponse) + assert len(response.data.columns) == 2 + assert len(response.data.rows) == 1 + assert response.data.rows[0].record_id == 1 + + def test_400(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(400)) + + request = GetReportByIdRequest(report_id=53) + + _assert_error(client.get_report_by_id(request), 400, "Invalid request based on underlying data") + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(401)) + + request = GetReportByIdRequest(report_id=53) + + _assert_error(client.get_report_by_id(request), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + request = GetReportByIdRequest(report_id=53) + + response = client.get_report_by_id(request) + + _assert_error(response, 403, "An error occurred") + + def test_404(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/999").mock(return_value=Response(404)) + + request = GetReportByIdRequest(report_id=999) + + _assert_error(client.get_report_by_id(request), 404, "Report could not be found") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(418)) + + request = GetReportByIdRequest(report_id=53) + + _assert_error(client.get_report_by_id(request), 418, None) + + def test_params_excludes_report_id(self, client: OnspringClient): + request = GetReportByIdRequest(report_id=53) + + with respx.mock: + route = respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(200, json=MOCK_REPORT_RESPONSE)) + client.get_report_by_id(request) + + assert "reportId" not in route.calls[0].request.url.params + + def test_403_empty_body(self, client: OnspringClient): + request = GetReportByIdRequest(report_id=53) + + with respx.mock: + respx.get(f"{TEST_URL}/Reports/id/53").mock(return_value=Response(403, content=b"")) + response = client.get_report_by_id(request) + + assert response.status_code == 403 + assert response.is_successful is False + + +class TestGetReportsByAppId: + def test_success(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/appId/10").mock( + return_value=Response(200, json=MOCK_REPORTS_BY_APP_RESPONSE) + ) + + response = client.get_reports_by_app_id(10) + + assert response.is_successful + assert isinstance(response.data, GetReportsByAppIdResponse) + assert len(response.data.reports) == 1 + assert response.data.reports[0].id == 53 + + def test_400(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/appId/10").mock(return_value=Response(400)) + + _assert_error(client.get_reports_by_app_id(10), 400, "Client does not have read access to the app.") + + def test_401(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/appId/10").mock(return_value=Response(401)) + + _assert_error(client.get_reports_by_app_id(10), 401, "Unauthorized request") + + def test_403_with_message(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/appId/10").mock(return_value=Response(403, json=MOCK_MESSAGE_RESPONSE)) + + response = client.get_reports_by_app_id(10) + + _assert_error(response, 403, "An error occurred") + + def test_fallthrough(self, client: OnspringClient): + with respx.mock: + respx.get(f"{TEST_URL}/Reports/appId/10").mock(return_value=Response(418)) + + _assert_error(client.get_reports_by_app_id(10), 418, None) + + def test_with_explicit_paging(self, client: OnspringClient): + from onspring_api_sdk.models import PagingRequest + + with respx.mock: + respx.get(f"{TEST_URL}/Reports/appId/10").mock( + return_value=Response(200, json=MOCK_REPORTS_BY_APP_RESPONSE) + ) + + paging = PagingRequest(page_number=2, page_size=10) + response = client.get_reports_by_app_id(10, paging_request=paging) + + assert response.is_successful + assert isinstance(response.data, GetReportsByAppIdResponse) + + +class TestRaiseForStatus: + def test_401_raises_authentication_error(self): + response = ApiResponse(status_code=401, message="Unauthorized") + + with pytest.raises(OnspringAuthenticationError, match="Unauthorized"): + response.raise_for_status() + + def test_403_raises_authentication_error(self): + response = ApiResponse(status_code=403, message="Forbidden") + + with pytest.raises(OnspringAuthenticationError, match="Forbidden"): + response.raise_for_status() + + def test_404_raises_not_found_error(self): + response = ApiResponse(status_code=404, message="Not Found") + + with pytest.raises(OnspringNotFoundError, match="Not Found"): + response.raise_for_status() + + def test_429_raises_rate_limit_error(self): + response = ApiResponse(status_code=429, message="Rate limited") + + with pytest.raises(OnspringRateLimitError, match="Rate limited"): + response.raise_for_status() + + def test_418_raises_generic_exception(self): + response = ApiResponse(status_code=418, message="Teapot") + + with pytest.raises(OnspringError, match="Teapot"): + response.raise_for_status() + + def test_success_does_not_raise(self): + response = ApiResponse(status_code=200, data="ok") + + response.raise_for_status()