diff --git a/.github/instructions/connector-table.instructions.md b/.github/instructions/connector-table.instructions.md index c0365df..e605be2 100644 --- a/.github/instructions/connector-table.instructions.md +++ b/.github/instructions/connector-table.instructions.md @@ -1,8 +1,28 @@ --- applyTo: "src/azure/connectors/*.py" --- -# Connector Table Maintenance +# Connector Code Maintenance -When a new connector module is added to `src/azure/connectors/`, update the supported SDK connector names list in `.github/skills/connection-setup/SKILL.md` (Step 2). Add the new connector's API name (e.g., `office365`, `sharepointonline`) to the inline list. +## Generated Code Rules -Also update the validated connectors table in `README.md` if the connector has been validated end-to-end. +Generated connector files in `src/azure/connectors/` are read-only. Do NOT hand-edit generated files. If the generated code has bugs: + +1. Fix the generator in BPM repo (`src/tools/CodefulSdkGenerator/DirectClient/DirectClientPythonCodeGenerator.cs`) +2. Regenerate the connector +3. Add the defect to the Known Generator Defects Registry in `GENERATION.md` + +See `GENERATION.md` for: +- **Generated Client Acceptance Criteria** — invariants every generated client must satisfy +- **Generator File Locations** — BPM repo paths for fixing generator bugs +- **Known Generator Defects Registry** — tracking known issues + +## Validation Checklist + +When a new connector module is added to `src/azure/connectors/`: + +1. Run the code quality tests: `pytest tests/test_code_quality.py -v` +2. Update the supported SDK connector names list in `.github/skills/connection-setup/SKILL.md` (Step 2) +3. Update the validated connectors table in `README.md` +4. Ensure unit tests cover: + - Request body assertions for create/update operations + - Error handling (non-2xx raises `ConnectorException`) for all operations including void ones diff --git a/CHANGELOG.md b/CHANGELOG.md index 26aaa02..cdd17bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **13 new connector clients** with unit tests and samples: - ARM (Azure Resource Manager), Azure AD, Azure Cosmos DB, Azure Event Hubs, Azure Queues, Azure Tables, Excel Online (Business), Microsoft Dataverse, Microsoft Defender ATP, Outlook, Service Bus, SMTP, Word Online (Business) +### Changed + +- **Comprehensive error handling improvements** across 14 connector clients: + - Added error handling tests for all HTTP operations (4xx/5xx responses) + - Fixed variable shadowing in `outlook.py` and `office365.py` (`respond_to_event_async`) + - Fixed incorrect `ConnectorException` signature in `teams.py` (2 methods) + ## [0.2.0b2] - 2026-05-13 ### Added diff --git a/GENERATION.md b/GENERATION.md index b8c370a..111c2c1 100644 --- a/GENERATION.md +++ b/GENERATION.md @@ -323,8 +323,10 @@ If a connector name is not recognized: If generated code has syntax errors or type issues: 1. Check BPM repository is up to date (especially PR 15456622 for `--python` support) -2. Report issues to BPM team with connector name and error details -3. Review swagger definition for edge cases +2. Fix the generator, not the generated output — see [Generator File Locations](#generator-file-locations-bpm-repository) +3. Report issues to BPM team with connector name and error details (file in [BPM Azure DevOps](https://dev.azure.com/msazure/One/_workitems)) +4. Add the defect to the [Known Generator Defects Registry](#known-generator-defects-registry) +5. Review swagger definition for edge cases ## Example Workflows @@ -357,6 +359,113 @@ LogicAppsCompiler.exe $outputDir unused --directClient --python --connectors=$co 4. **Testing** - Always run full test suite after regeneration 5. **Documentation** - Keep README and ROADMAP in sync with available connectors +## Generated Client Acceptance Criteria + +Every generated connector client MUST satisfy these invariants before merge: + +### 1. Request Body Handling (Create/Update Operations) + +- Every `POST`, `PUT`, or `PATCH` operation that accepts input parameters MUST construct and send a request body +- All input parameters marked as body parameters in swagger MUST be serialized into the request +- Create/update operations MUST NOT silently drop input parameters + +**Validation:** Unit tests MUST assert that the outgoing request body contains expected fields for create/update operations. + +### 2. Response Status Checking (All Operations) + +- Every HTTP operation MUST capture the response and check the status code +- Non-2xx responses MUST raise `ConnectorException` with status code and message +- Void operations (DELETE, PATCH, fire-and-forget POST) MUST NOT discard the response unchecked + +**Correct pattern:** +```python +response = await self.http_client.send_async( + method="DELETE", + url=request_uri, +) +if not (200 <= response.status < 300): + raise ConnectorException( + f"Operation failed with status {response.status}: {await response.text()}" + ) +``` + +**Incorrect pattern (MUST NOT pass review):** +```python +await self.http_client.send_async( # Result discarded - silent failures! + method="DELETE", + url=request_uri, +) +``` + +**Validation:** The `test_no_unchecked_send_async_calls` test in `tests/test_code_quality.py` scans for unchecked `send_async` calls. Additionally, unit tests for void methods MUST assert that non-2xx responses raise `ConnectorException`. + +### 3. Test Coverage + +- No skipped tests without a tracked issue link in the skip reason +- Each public method MUST have tests for: success case, error response handling, parameter serialization +- Void methods MUST have explicit tests verifying non-2xx raises exception + +**Validation:** Tests with `@pytest.mark.skip` MUST include a GitHub issue link (e.g., `reason="Template variable bug - see #123"`). + +### 4. Type Completeness + +- All public types (dataclasses) referenced by method signatures MUST be defined +- No `# type: ignore` comments unless accompanied by a tracked issue link +- All optional parameters MUST have `Optional[T]` type hints and default values + +## Generator File Locations (BPM Repository) + +When fixing generator bugs (per the "fix the generator, not generated output" rule), these are the key files: + +### Python Generator + +| File | Purpose | +|------|--------| +| `src/tools/CodefulSdkGenerator/DirectClient/DirectClientPythonGenerator.cs` | Entry point for Python generation; orchestrates the generation pipeline | +| `src/tools/CodefulSdkGenerator/DirectClient/DirectClientPythonCodeGenerator.cs` | Core code emitter; generates Python client classes, methods, and type definitions | + +### Key Differences from .NET Generator + +The .NET SDK centralizes error handling in `ConnectorClientBase.CallConnectorAsync()` — this base method auto-throws on non-2xx for both value-returning and void operations. The Python emitter must explicitly generate the status check in each method body because Python's `http_client.send_async()` does not auto-raise. + +**If you find a Python generator bug:** +1. Fix it in `DirectClientPythonCodeGenerator.cs` +2. Add a test case in `CodefulSdkGenerator.Tests/` +3. Regenerate affected connectors +4. Add the defect to the Known Generator Defects Registry below + +### C# Generator + +| File | Purpose | +|------|--------| +| `src/tools/CodefulSdkGenerator/DirectClient/DirectClientCSharpGenerator.cs` | C# generation entry point | +| `src/tools/CodefulSdkGenerator/DirectClient/DirectClientCSharpCodeGenerator.cs` | C# code emitter | + +### Shared Infrastructure + +| File | Purpose | +|------|--------| +| `src/tools/CodefulSdkGenerator/ConnectorsGenerator.cs` | ARM API fetching, swagger parsing, JSON sanitization | +| `src/tools/CodefulSdkGenerator/Data/ManagedConnectorSkipList.txt` | Connectors skipped due to known issues | +| `src/tools/CodefulSdkGenerator/Data/*.txt` | Character replacement and reformatting lists | + +## Known Generator Defects Registry + +Track known generator issues here to prevent silent recurrence across releases. + +| Defect | BPM Issue | Affected Connectors | Status | Notes | +|--------|-----------|---------------------|--------|-------| +| Teams template variable causes NameError at import | [BPM #TBD](https://dev.azure.com/msazure/One/_workitems/edit/TBD) | `teams` | Workaround (skip import test) | `DynamicValueLookup` references undefined variable; test skipped with issue link | +| Void operations not checking response status | Fixed in generator | Pre-2024 connectors | Fixed | Regenerate affected connectors: `azurequeues`, `azuretables`, `commondataservice`, `eventhubs`, `excelonlinebusiness`, `office365`, `onedrive`, `outlook`, `servicebus`, `sharepointonline`, `smtp`, `azuread` | +| api_version parameters missing default values | Fixed in generator | `azuredigitaltwins`, `azurevm`, others | Fixed | Generator now uses swagger default value for api_version/api-version parameters | +| Create/update operations missing request body | [BPM #TBD](https://dev.azure.com/msazure/One/_workitems/edit/TBD) | Varies by connector | Investigation | Some POST/PATCH operations may drop body parameters | + +**Adding a new defect:** +1. File an issue in BPM Azure DevOps +2. Add a row to this table with connector list +3. Add `@pytest.mark.skip(reason="Generator defect - see GENERATION.md")` to affected tests +4. Update status when fixed and connectors regenerated + ## Related Documentation - [README.md](README.md) - SDK overview and quick start diff --git a/README.md b/README.md index 92d1b09..97d543e 100644 --- a/README.md +++ b/README.md @@ -154,40 +154,40 @@ The following connectors have been generated and validated with comprehensive te | Connector | Package | Status | Coverage | Tests | |-----------|---------|--------|----------|-------| -| **ARM (Azure Resource Manager)** | `azure.connectors.arm` | ✅ Complete | 🔄 SDK Generated | 39 tests | +| **ARM (Azure Resource Manager)** | `azure.connectors.arm` | ✅ Complete | 🔄 SDK Generated | 57 tests | | **Azure Automation** | `azure.connectors.azureautomation` | ✅ Complete | 🔄 SDK Generated | 32 tests | -| **Azure AD** | `azure.connectors.azuread` | ✅ Complete | 🔄 SDK Generated | 35 tests | -| **Azure Blob Storage** | `azure.connectors.azureblob` | ✅ Complete | ✅ E2E Validated | 52 tests | +| **Azure AD** | `azure.connectors.azuread` | ✅ Complete | 🔄 SDK Generated | 68 tests | +| **Azure Blob Storage** | `azure.connectors.azureblob` | ✅ Complete | ✅ E2E Validated | 51 tests | | **Azure Cosmos DB** | `azure.connectors.documentdb` | ✅ Complete | 🔄 SDK Generated | 46 tests | | **Azure Data Factory** | `azure.connectors.azuredatafactory` | ✅ Complete | 🔄 SDK Generated | 38 tests | | **Azure Digital Twins** | `azure.connectors.azuredigitaltwins` | ✅ Complete | 🔄 SDK Generated | 44 tests | | **Azure Data Explorer** | `azure.connectors.kusto` | ✅ Complete | ✅ E2E Validated | 37 tests | -| **Azure Event Hubs** | `azure.connectors.eventhubs` | ✅ Complete | 🔄 SDK Generated | 30 tests | +| **Azure Event Hubs** | `azure.connectors.eventhubs` | ✅ Complete | 🔄 SDK Generated | 32 tests | | **Azure Key Vault** | `azure.connectors.keyvault` | ✅ Complete | 🔄 SDK Generated | 42 tests | -| **Azure Queues** | `azure.connectors.azurequeues` | ✅ Complete | 🔄 SDK Generated | 34 tests | -| **Azure Tables** | `azure.connectors.azuretables` | ✅ Complete | 🔄 SDK Generated | 43 tests | +| **Azure Queues** | `azure.connectors.azurequeues` | ✅ Complete | 🔄 SDK Generated | 38 tests | +| **Azure Tables** | `azure.connectors.azuretables` | ✅ Complete | 🔄 SDK Generated | 53 tests | | **Azure VM** | `azure.connectors.azurevm` | ✅ Complete | 🔄 SDK Generated | 43 tests | -| **Excel Online (Business)** | `azure.connectors.excelonlinebusiness` | ✅ Complete | 🔄 SDK Generated | 37 tests | -| **IBM MQ** | `azure.connectors.mq` | ✅ Complete | ✅ E2E Validated | 30 tests | +| **Excel Online (Business)** | `azure.connectors.excelonlinebusiness` | ✅ Complete | 🔄 SDK Generated | 53 tests | +| **IBM MQ** | `azure.connectors.mq` | ✅ Complete | ✅ E2E Validated | 33 tests | | **Microsoft Bookings** | `azure.connectors.microsoftbookings` | ✅ Complete | 🔄 SDK Generated | 36 tests | -| **Microsoft Dataverse** | `azure.connectors.commondataservice` | ✅ Complete | 🔄 SDK Generated | 46 tests | +| **Microsoft Dataverse** | `azure.connectors.commondataservice` | ✅ Complete | 🔄 SDK Generated | 53 tests | | **Microsoft Defender ATP** | `azure.connectors.wdatp` | ✅ Complete | 🔄 SDK Generated | 32 tests | | **Microsoft Graph** | `azure.connectors.msgraphgroupsanduser` | ✅ Complete | ✅ E2E Validated | 46 tests | -| **Microsoft Teams** | `azure.connectors.teams` | ✅ Complete | ✅ E2E Validated | 27 tests | -| **Office 365 Outlook** | `azure.connectors.office365` | ✅ Complete | ✅ E2E Validated | 41 tests | -| **Office 365 Users** | `azure.connectors.office365users` | ✅ Complete | ✅ E2E Validated | 40 tests | +| **Microsoft Teams** | `azure.connectors.teams` | ✅ Complete | ✅ E2E Validated | 44 tests | +| **Office 365 Outlook** | `azure.connectors.office365` | ✅ Complete | ✅ E2E Validated | 72 tests | +| **Office 365 Users** | `azure.connectors.office365users` | ✅ Complete | ✅ E2E Validated | 46 tests | | **Office 365 Groups** | `azure.connectors.office365groups` | ✅ Complete | 🔄 SDK Generated | 50 tests | -| **OneDrive for Business** | `azure.connectors.onedrive` | ✅ Complete | 🔄 SDK Generated | 41 tests | +| **OneDrive for Business** | `azure.connectors.onedrive` | ✅ Complete | 🔄 SDK Generated | 49 tests | | **OneNote** | `azure.connectors.onenote` | ✅ Complete | 🔄 SDK Generated | 60 tests | -| **Outlook.com** | `azure.connectors.outlook` | ✅ Complete | 🔄 SDK Generated | 42 tests | +| **Outlook.com** | `azure.connectors.outlook` | ✅ Complete | 🔄 SDK Generated | 49 tests | | **Planner** | `azure.connectors.planner` | ✅ Complete | 🔄 SDK Generated | 66 tests | -| **Service Bus** | `azure.connectors.servicebus` | ✅ Complete | 🔄 SDK Generated | 37 tests | -| **SharePoint Online** | `azure.connectors.sharepointonline` | ✅ Complete | ✅ E2E Validated | 44 tests | -| **SMTP** | `azure.connectors.smtp` | ✅ Complete | 🔄 SDK Generated | 27 tests | +| **Service Bus** | `azure.connectors.servicebus` | ✅ Complete | 🔄 SDK Generated | 52 tests | +| **SharePoint Online** | `azure.connectors.sharepointonline` | ✅ Complete | ✅ E2E Validated | 80 tests | +| **SMTP** | `azure.connectors.smtp` | ✅ Complete | 🔄 SDK Generated | 28 tests | | **Word Online (Business)** | `azure.connectors.wordonlinebusiness` | ✅ Complete | 🔄 SDK Generated | 30 tests | | **Yammer** | `azure.connectors.yammer` | ✅ Complete | 🔄 SDK Generated | 38 tests | -**Total:** 685 connector tests + 110 SDK component tests +**Total:** 1049 connector tests + 141 SDK component tests See [ROADMAP.md](ROADMAP.md) for planned connector additions and [tests/README.md](tests/README.md) for detailed test coverage. diff --git a/src/azure/connectors/arm.py b/src/azure/connectors/arm.py index 2b7ecb2..a707ba1 100644 --- a/src/azure/connectors/arm.py +++ b/src/azure/connectors/arm.py @@ -7,6 +7,7 @@ from dataclasses import dataclass, field from typing import Optional, Any, Dict, List +from urllib.parse import quote import json from azure.connectors.sdk import ( @@ -662,8 +663,85 @@ def __init__( def connector_name(self) -> str: return "arm" + async def subscriptions_list_locations_async( + self, + subscription_id: str, + x_ms_api_version: Optional[str], + ): + """ + Lists the subscription locations + + Lists the locations available for the subscription. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions/{str(subscription_id)}/locations" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def subscriptions_get_async( + self, + subscription_id: str, + x_ms_api_version: Optional[str], + ): + """ + Read a subscription + + Reads the details for a particular subscription. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions/{str(subscription_id)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + async def subscriptions_list_async( self, + x_ms_api_version: Optional[str], ): """ List subscriptions @@ -671,6 +749,1510 @@ async def subscriptions_list_async( Gets a list of all the subscriptions to which the principal has access. """ path = f"{self._connection_runtime_url}/subscriptions" + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def deployments_get_async( + self, + subscription_id: str, + resource_group_name: str, + deployment_name: str, + x_ms_api_version: Optional[str], + wait: Optional[str] = None, + ): + """ + Read a template deployment + + Reads a template deployment within a resource group. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/providers" + f"/Microsoft.Resources" + f"/deployments" + f"/{str(deployment_name)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if wait is not None: + value = str(wait) + if isinstance(wait, bool): + value = value.lower() + query_params.append(f"wait={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def deployments_create_or_update_async( + self, + input: Deployment, + subscription_id: str, + resource_group_name: str, + deployment_name: str, + x_ms_api_version: Optional[str], + wait: Optional[str] = None, + ): + """ + Create or update a template deployment + + Create or update a named resource group template deployment. A template + and parameters are expected for the request to succeed. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/providers" + f"/Microsoft.Resources" + f"/deployments" + f"/{str(deployment_name)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if wait is not None: + value = str(wait) + if isinstance(wait, bool): + value = value.lower() + query_params.append(f"wait={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("PUT", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PUT", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def deployments_delete_async( + self, + subscription_id: str, + resource_group_name: str, + deployment_name: str, + x_ms_api_version: Optional[str], + ): + """ + Delete template deployment + + Deletes a resource group template deployment. The resources will not be + deleted; only the metadata about the template deployment. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/providers" + f"/Microsoft.Resources" + f"/deployments" + f"/{str(deployment_name)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) + + async def deployments_cancel_async( + self, + subscription_id: str, + resource_group_name: str, + deployment_name: str, + x_ms_api_version: Optional[str], + ): + """ + Cancel a template deployment + + Cancel a currently running template deployment. All pending template + operations will be suspended. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/providers" + f"/Microsoft.Resources" + f"/deployments" + f"/{str(deployment_name)}" + f"/cancel" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + async def deployments_validate_async( + self, + input: Deployment, + subscription_id: str, + resource_group_name: str, + deployment_name: str, + x_ms_api_version: Optional[str], + ): + """ + Validate a template deployment + + Validates a deployment template. This operation does not have side + effects and can be used to test a template deployment for syntax or + logical errors. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/providers" + f"/Microsoft.Resources" + f"/deployments" + f"/{str(deployment_name)}" + f"/validate" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def deployments_export_template_async( + self, + subscription_id: str, + resource_group_name: str, + deployment_name: str, + x_ms_api_version: Optional[str], + ): + """ + Export deployment template + + Exports a template from a past resource group deployment. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/providers" + f"/Microsoft.Resources" + f"/deployments" + f"/{str(deployment_name)}" + f"/exportTemplate" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def deployments_list_async( + self, + subscription_id: str, + resource_group_name: str, + x_ms_api_version: Optional[str], + filter: Optional[str] = None, + top: Optional[str] = None, + ): + """ + List template deployments + + Lists all the resource group template deployments. This operation is + useful to know what has been provisioned thus far. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/providers" + f"/Microsoft.Resources" + f"/deployments" + ) + query_params = [] + if filter is not None: + value = str(filter) + if isinstance(filter, bool): + value = value.lower() + query_params.append(f"$filter={quote(value)}") + if top is not None: + value = str(top) + if isinstance(top, bool): + value = value.lower() + query_params.append(f"$top={quote(value)}") + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def deployment_operations_get_async( + self, + subscription_id: str, + resource_group_name: str, + deployment_name: str, + operation_id: str, + x_ms_api_version: Optional[str], + ): + """ + Read a template deployment operation + + Reads a particular resource group template deployment operation. This + is useful for troubleshooting failed template deployments. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/deployments" + f"/{str(deployment_name)}" + f"/operations" + f"/{str(operation_id)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def deployment_operations_list_async( + self, + subscription_id: str, + resource_group_name: str, + deployment_name: str, + x_ms_api_version: Optional[str], + top: Optional[str] = None, + ): + """ + Lists template deployment operations + + Lists all the template deployment operations. This is useful for + troubleshooting failed template deployments. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/deployments" + f"/{str(deployment_name)}" + f"/operations" + ) + query_params = [] + if top is not None: + value = str(top) + if isinstance(top, bool): + value = value.lower() + query_params.append(f"$top={quote(value)}") + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def providers_unregister_async( + self, + subscription_id: str, + resource_provider_namespace: str, + x_ms_api_version: Optional[str], + ): + """ + Unregister resource provider + + Unregisters provider from a subscription. This operation will fail if + there are any resources from that resource provider in the + subscription. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/providers" + f"/{str(resource_provider_namespace)}" + f"/unregister" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def providers_register_async( + self, + subscription_id: str, + resource_provider_namespace: str, + x_ms_api_version: Optional[str], + ): + """ + Register resource provider + + Registers a resource provider to be used with a subscription. This will + provision permissions for the service into your subscription. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/providers" + f"/{str(resource_provider_namespace)}" + f"/register" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def providers_list_async( + self, + subscription_id: str, + x_ms_api_version: Optional[str], + top: Optional[str] = None, + expand: Optional[str] = None, + ): + """ + List resource providers + + Lists the resource providers available for the subscription. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions/{str(subscription_id)}/providers" + ) + query_params = [] + if top is not None: + value = str(top) + if isinstance(top, bool): + value = value.lower() + query_params.append(f"$top={quote(value)}") + if expand is not None: + value = str(expand) + if isinstance(expand, bool): + value = value.lower() + query_params.append(f"$expand={quote(value)}") + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def providers_get_async( + self, + subscription_id: str, + resource_provider_namespace: str, + x_ms_api_version: Optional[str], + expand: Optional[str] = None, + ): + """ + Read resource provider + + Reads a particular resource provider within the subscription. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/providers" + f"/{str(resource_provider_namespace)}" + ) + query_params = [] + if expand is not None: + value = str(expand) + if isinstance(expand, bool): + value = value.lower() + query_params.append(f"$expand={quote(value)}") + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def resource_groups_list_resources_async( + self, + subscription_id: str, + resource_group_name: str, + x_ms_api_version: Optional[str], + filter: Optional[str] = None, + expand: Optional[str] = None, + top: Optional[str] = None, + ): + """ + List resources by resource group + + Lists all the resources under a resource group. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourceGroups" + f"/{str(resource_group_name)}" + f"/resources" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if filter is not None: + value = str(filter) + if isinstance(filter, bool): + value = value.lower() + query_params.append(f"$filter={quote(value)}") + if expand is not None: + value = str(expand) + if isinstance(expand, bool): + value = value.lower() + query_params.append(f"$expand={quote(value)}") + if top is not None: + value = str(top) + if isinstance(top, bool): + value = value.lower() + query_params.append(f"$top={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def resource_groups_get_async( + self, + subscription_id: str, + resource_group_name: str, + x_ms_api_version: Optional[str], + ): + """ + Read a resource group + + Reads a particular resource group within the subscription. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def resource_groups_create_or_update_async( + self, + input: ResourceGroup, + subscription_id: str, + resource_group_name: str, + x_ms_api_version: Optional[str], + ): + """ + Create or update a resource group + + Creates or updates a resource group. The response code can be used to + distinguish between a create (201) or update (200). + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("PUT", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PUT", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def resource_groups_delete_async( + self, + subscription_id: str, + resource_group_name: str, + x_ms_api_version: Optional[str], + ): + """ + Delete a resource group + + Delete a particular resource group within the subscription. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) + + async def resource_groups_patch_async( + self, + input: ResourceGroup, + subscription_id: str, + resource_group_name: str, + x_ms_api_version: Optional[str], + ): + """ + Update an existing resource group + + Updates an existing resource group. If the resource does not exist, + this request will fail. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("PATCH", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PATCH", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def resource_groups_export_template_async( + self, + input: ExportTemplateRequest, + subscription_id: str, + resource_group_name: str, + x_ms_api_version: Optional[str], + ): + """ + Export a resource group template + + Exports a deployment template from an existing resource group. This can + only be successful if the underlying resources have a schema defined by + Microsoft. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/exportTemplate" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def resource_groups_list_async( + self, + subscription_id: str, + x_ms_api_version: Optional[str], + filter: Optional[str] = None, + top: Optional[str] = None, + ): + """ + List resource groups + + Lists all the resource groups within the subscription. The results are + paginated at 1,000+ records. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions/{str(subscription_id)}/resourcegroups" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if filter is not None: + value = str(filter) + if isinstance(filter, bool): + value = value.lower() + query_params.append(f"$filter={quote(value)}") + if top is not None: + value = str(top) + if isinstance(top, bool): + value = value.lower() + query_params.append(f"$top={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def resources_list_async( + self, + subscription_id: str, + x_ms_api_version: Optional[str], + filter: Optional[str] = None, + expand: Optional[str] = None, + top: Optional[str] = None, + ): + """ + List resources by subscription + + Reads all of the resources under a particular subscription. The results + are paginated at 1,000+ records. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions/{str(subscription_id)}/resources" + ) + query_params = [] + if filter is not None: + value = str(filter) + if isinstance(filter, bool): + value = value.lower() + query_params.append(f"$filter={quote(value)}") + if expand is not None: + value = str(expand) + if isinstance(expand, bool): + value = value.lower() + query_params.append(f"$expand={quote(value)}") + if top is not None: + value = str(top) + if isinstance(top, bool): + value = value.lower() + query_params.append(f"$top={quote(value)}") + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def resources_get_by_id_async( + self, + subscription_id: str, + resource_group_name: str, + resource_provider_namespace: str, + short_resource_id: str, + x_ms_api_version: Optional[str], + ): + """ + Read a resource + + Reads a resource object. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/providers" + f"/{str(resource_provider_namespace)}" + f"/{str(short_resource_id)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def resources_create_or_update_by_id_async( + self, + input: GenericResource, + subscription_id: str, + resource_group_name: str, + resource_provider_namespace: str, + short_resource_id: str, + x_ms_api_version: Optional[str], + ): + """ + Create or update a resource + + Creates or updates a resource. The response code can be used to + distinguish between a create (201) or update (200). + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/providers" + f"/{str(resource_provider_namespace)}" + f"/{str(short_resource_id)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("PUT", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PUT", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def resources_delete_by_id_async( + self, + subscription_id: str, + resource_group_name: str, + resource_provider_namespace: str, + short_resource_id: str, + x_ms_api_version: Optional[str], + ): + """ + Delete a resource + + Deletes a resource. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/providers" + f"/{str(resource_provider_namespace)}" + f"/{str(short_resource_id)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) + + async def resources_invoke_async( + self, + input: ResourcesInvokeInput, + subscription_id: str, + resource_group_name: str, + resource_provider_namespace: str, + short_resource_id: str, + action_name: str, + x_ms_api_version: Optional[str], + ): + """ + Invoke resource operation + + Invokes an operation on an Azure resource. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/resourcegroups" + f"/{str(resource_group_name)}" + f"/providers" + f"/{str(resource_provider_namespace)}" + f"/{str(short_resource_id)}" + f"/{str(action_name)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def provider_resources_get_by_id_async( + self, + subscription_id: str, + resource_provider_namespace: str, + short_resource_id: str, + x_ms_api_version: Optional[str], + ): + """ + Read a resource in provider + + Reads a resource object. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/providers" + f"/{str(resource_provider_namespace)}" + f"/{str(short_resource_id)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def provider_resources_invoke_async( + self, + input: ProviderResourcesInvokeInput, + subscription_id: str, + resource_provider_namespace: str, + short_resource_id: str, + x_ms_api_version: Optional[str], + ): + """ + Invoke resource operation in provider + + Invokes an operation on an Azure resource. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/providers" + f"/{str(resource_provider_namespace)}" + f"/{str(short_resource_id)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def tags_create_or_update_value_async( + self, + subscription_id: str, + tag_name: str, + tag_value: str, + x_ms_api_version: Optional[str], + ): + """ + Create or update a subscription resource tag value + + Create or update a subscription resource tag value. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/tagNames" + f"/{str(tag_name)}" + f"/tagValues" + f"/{str(tag_value)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("PUT", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PUT", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def tags_delete_value_async( + self, + subscription_id: str, + tag_name: str, + tag_value: str, + x_ms_api_version: Optional[str], + ): + """ + Delete a subscription resource tag value + + Delete a subscription resource tag value. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions" + f"/{str(subscription_id)}" + f"/tagNames" + f"/{str(tag_name)}" + f"/tagValues" + f"/{str(tag_value)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) + + async def tags_create_or_update_async( + self, + subscription_id: str, + tag_name: str, + x_ms_api_version: Optional[str], + ): + """ + Create or update a subscription resource tag name + + Create or update a subscription resource tag name. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions/{str(subscription_id)}/tagNames/{str(tag_name)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("PUT", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PUT", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def tags_delete_async( + self, + subscription_id: str, + tag_name: str, + x_ms_api_version: Optional[str], + ): + """ + Delete a subscription resource tag name + + Delete a subscription resource tag name. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions/{str(subscription_id)}/tagNames/{str(tag_name)}" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) + + async def tags_list_async( + self, + subscription_id: str, + x_ms_api_version: Optional[str], + ): + """ + List subscription resource tags + + Lists all the subscription resource tags. + """ + path = ( + f"{self._connection_runtime_url}" + f"/subscriptions/{str(subscription_id)}/tagNames" + ) + query_params = [] + if x_ms_api_version is not None: + value = str(x_ms_api_version) + if isinstance(x_ms_api_version, bool): + value = value.lower() + query_params.append(f"x-ms-api-version={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) response = await self.http_client.send_async("GET", path, body=None) diff --git a/src/azure/connectors/azuread.py b/src/azure/connectors/azuread.py index 17167c8..46e9601 100644 --- a/src/azure/connectors/azuread.py +++ b/src/azure/connectors/azuread.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from typing import Optional, Any, Dict, List +from urllib.parse import quote import json from azure.connectors.sdk import ( @@ -399,6 +400,104 @@ async def create_security_group_async( return json.loads(response.text) + async def get_group_async( + self, + id: str, + ): + """ + Get group + + Get details for a group. + """ + path = f"{self._connection_runtime_url}/v1.0/groups/{str(id)}" + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def get_user_async( + self, + id: str, + ): + """ + Get user + + Get details for a user. + """ + path = f"{self._connection_runtime_url}/v1.0/users/{str(id)}" + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def update_user_async( + self, + input: UpdateUserRequest, + id: str, + ): + """ + Update user + + Update the info for a user. + """ + path = f"{self._connection_runtime_url}/v1.0/users/{str(id)}" + + response = await self.http_client.send_async("PATCH", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PATCH", + path, + response.status, + response.text, + ) + + async def refresh_tokens_async( + self, + id: str, + ): + """ + Refresh tokens + + Invalidate all refresh tokens for a user + """ + path = ( + f"{self._connection_runtime_url}" + f"/v1.0/users/{str(id)}/revokeSignInSessions" + ) + + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + async def create_user_async( self, input: CreateUserRequest, @@ -425,6 +524,44 @@ async def create_user_async( return json.loads(response.text) + async def get_group_members_async( + self, + id: str, + top: Optional[str] = None, + ): + """ + Get group members + + Get the users who are members of a group. You can query up to 1000 + items using the Top parameter. If you need to retrieve more than 1000 + values, please turn on the Settings->Pagination feature and provide a + Threshold limit. + """ + path = f"{self._connection_runtime_url}/v1.0/groups/{str(id)}/members" + query_params = [] + if top is not None: + value = str(top) + if isinstance(top, bool): + value = value.lower() + query_params.append(f"$top={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + async def remove_member_from_group_async( self, group_id: str, @@ -440,7 +577,64 @@ async def remove_member_from_group_async( f"/v1.0/groups/{str(group_id)}/members/{str(member_id)}/$ref" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) + + async def add_user_to_group_async( + self, + input: GetGroupRequest, + id: str, + ): + """ + Add user to group + + Add a user to a group in this Microsoft Entra ID tenant. + """ + path = ( + f"{self._connection_runtime_url}" + f"/v1.0/groups/{str(id)}/members/$ref" + ) + + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + async def assign_manager_async( + self, + input: AssignManagerRequest, + id: str, + ): + """ + Assign manager + + Assign a manager for a user. + """ + path = ( + f"{self._connection_runtime_url}/v1.0/users/{str(id)}/manager/$ref" + ) + + response = await self.http_client.send_async("PUT", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PUT", + path, + response.status, + response.text, + ) async def create_group_async( self, @@ -467,3 +661,64 @@ async def create_group_async( return None return json.loads(response.text) + + async def check_member_groups_async( + self, + input: CheckMemberGroupsRequest, + id: str, + ): + """ + Check group membership (V2) + + If the user is a member of the given group, the result will contain the + given id. Otherwise the result will be empty. + """ + path = ( + f"{self._connection_runtime_url}" + f"/v2/v1.0/users/{str(id)}/checkMemberGroups" + ) + + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def get_member_groups_async( + self, + input: GetMemberGroupsRequest, + id: str, + ): + """ + Get groups of a user (V2) + + Get the groups a user is a member of. + """ + path = ( + f"{self._connection_runtime_url}" + f"/v2/v1.0/users/{str(id)}/getMemberGroups" + ) + + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) diff --git a/src/azure/connectors/azurequeues.py b/src/azure/connectors/azurequeues.py index b8469e5..e3f4184 100644 --- a/src/azure/connectors/azurequeues.py +++ b/src/azure/connectors/azurequeues.py @@ -169,7 +169,15 @@ async def delete_message_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def get_messages_async( self, @@ -362,7 +370,15 @@ async def put_message_async( f"/messages" ) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def put_queue_async( self, diff --git a/src/azure/connectors/azuretables.py b/src/azure/connectors/azuretables.py index f507411..a2331f3 100644 --- a/src/azure/connectors/azuretables.py +++ b/src/azure/connectors/azuretables.py @@ -298,7 +298,15 @@ async def delete_entity_async( f"/etag(PartitionKey='{str(partition_key)}',RowKey='{str(row_key)}')" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def delete_table_async( self, @@ -319,7 +327,15 @@ async def delete_table_async( f"/{str(table_name)}" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def get_entities_async( self, @@ -517,7 +533,15 @@ async def insert_merge_entity_async( f"/entities(PartitionKey='{str(partition_key)}',RowKey='{str(row_key)}')" ) - await self.http_client.send_async("PATCH", path, body=input) + response = await self.http_client.send_async("PATCH", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PATCH", + path, + response.status, + response.text, + ) async def insert_replace_entity_async( self, @@ -543,7 +567,15 @@ async def insert_replace_entity_async( f"/entities(PartitionKey='{str(partition_key)}',RowKey='{str(row_key)}')" ) - await self.http_client.send_async("PUT", path, body=input) + response = await self.http_client.send_async("PUT", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PUT", + path, + response.status, + response.text, + ) async def merge_entity_async( self, @@ -569,7 +601,15 @@ async def merge_entity_async( f"/etag(PartitionKey='{str(partition_key)}',RowKey='{str(row_key)}')" ) - await self.http_client.send_async("PATCH", path, body=input) + response = await self.http_client.send_async("PATCH", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PATCH", + path, + response.status, + response.text, + ) async def replace_entity_async( self, @@ -595,4 +635,12 @@ async def replace_entity_async( f"/etag(PartitionKey='{str(partition_key)}',RowKey='{str(row_key)}')" ) - await self.http_client.send_async("PUT", path, body=input) + response = await self.http_client.send_async("PUT", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PUT", + path, + response.status, + response.text, + ) diff --git a/src/azure/connectors/commondataservice.py b/src/azure/connectors/commondataservice.py index b733b75..9356165 100644 --- a/src/azure/connectors/commondataservice.py +++ b/src/azure/connectors/commondataservice.py @@ -644,7 +644,15 @@ async def subscribe_webhook_trigger_async( f"/api/data/v9.1/callbackregistrations" ) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def list_records_async( self, @@ -837,7 +845,15 @@ async def delete_record_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def update_record_async( self, @@ -906,7 +922,15 @@ async def update_entity_file_image_field_content_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("PUT", path, body=input) + response = await self.http_client.send_async("PUT", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "PUT", + path, + response.status, + response.text, + ) async def get_entity_file_image_field_content_async( self, @@ -950,7 +974,7 @@ async def get_entity_file_image_field_content_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def perform_unbound_action_async( self, @@ -1044,7 +1068,15 @@ async def associate_entities_async( f"/$ref" ) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def disassociate_entities_async( self, @@ -1079,7 +1111,15 @@ async def disassociate_entities_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def get_relevant_rows_async( self, @@ -1124,7 +1164,15 @@ async def execute_changeset_async( """ path = f"{self._connection_runtime_url}/api/data/v9.1/$batch" - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def business_events_trigger_async( self, @@ -1140,4 +1188,12 @@ async def business_events_trigger_async( f"/api/data/v9.2/callbackregistrations" ) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) diff --git a/src/azure/connectors/eventhubs.py b/src/azure/connectors/eventhubs.py index 413393e..7f8a36d 100644 --- a/src/azure/connectors/eventhubs.py +++ b/src/azure/connectors/eventhubs.py @@ -200,7 +200,15 @@ async def send_event_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def send_events_async( self, @@ -226,4 +234,12 @@ async def send_events_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) diff --git a/src/azure/connectors/excelonlinebusiness.py b/src/azure/connectors/excelonlinebusiness.py index f73e8c9..f1e5cd0 100644 --- a/src/azure/connectors/excelonlinebusiness.py +++ b/src/azure/connectors/excelonlinebusiness.py @@ -497,7 +497,15 @@ async def create_id_column_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def get_items_async( self, @@ -512,6 +520,8 @@ async def get_items_async( select: Optional[str] = None, id_column: Optional[str] = None, date_time_format: Optional[str] = None, + extract_sensitivity_label: Optional[str] = None, + fetch_sensitivity_label_metadata: Optional[str] = None, ): """ List rows present in a table @@ -563,6 +573,16 @@ async def get_items_async( if isinstance(date_time_format, bool): value = value.lower() query_params.append(f"dateTimeFormat={quote(value)}") + if extract_sensitivity_label is not None: + value = str(extract_sensitivity_label) + if isinstance(extract_sensitivity_label, bool): + value = value.lower() + query_params.append(f"extractSensitivityLabel={quote(value)}") + if fetch_sensitivity_label_metadata is not None: + value = str(fetch_sensitivity_label_metadata) + if isinstance(fetch_sensitivity_label_metadata, bool): + value = value.lower() + query_params.append(f"fetchSensitivityLabelMetadata={quote(value)}") if query_params: path += '?' + '&'.join(query_params) @@ -723,6 +743,8 @@ async def get_item_async( source: Optional[str], id_column: Optional[str], date_time_format: Optional[str] = None, + extract_sensitivity_label: Optional[str] = None, + fetch_sensitivity_label_metadata: Optional[str] = None, ): """ Get a row @@ -756,6 +778,16 @@ async def get_item_async( if isinstance(date_time_format, bool): value = value.lower() query_params.append(f"dateTimeFormat={quote(value)}") + if extract_sensitivity_label is not None: + value = str(extract_sensitivity_label) + if isinstance(extract_sensitivity_label, bool): + value = value.lower() + query_params.append(f"extractSensitivityLabel={quote(value)}") + if fetch_sensitivity_label_metadata is not None: + value = str(fetch_sensitivity_label_metadata) + if isinstance(fetch_sensitivity_label_metadata, bool): + value = value.lower() + query_params.append(f"fetchSensitivityLabelMetadata={quote(value)}") if query_params: path += '?' + '&'.join(query_params) @@ -813,7 +845,15 @@ async def delete_item_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def patch_item_async( self, @@ -885,6 +925,231 @@ async def patch_item_async( return json.loads(response.text) + async def get_all_worksheets_async( + self, + drive: str, + file: str, + source: Optional[str], + extract_sensitivity_label: Optional[str] = None, + fetch_sensitivity_label_metadata: Optional[str] = None, + ): + """ + Get worksheets + + Get a list of worksheets in the Excel workbook. + """ + path = ( + f"{self._connection_runtime_url}" + f"/codeless" + f"/v1.0" + f"/drives" + f"/{str(drive)}" + f"/items" + f"/{str(file)}" + f"/workbook" + f"/worksheets" + ) + query_params = [] + if source is not None: + value = str(source) + if isinstance(source, bool): + value = value.lower() + query_params.append(f"source={quote(value)}") + if extract_sensitivity_label is not None: + value = str(extract_sensitivity_label) + if isinstance(extract_sensitivity_label, bool): + value = value.lower() + query_params.append(f"extractSensitivityLabel={quote(value)}") + if fetch_sensitivity_label_metadata is not None: + value = str(fetch_sensitivity_label_metadata) + if isinstance(fetch_sensitivity_label_metadata, bool): + value = value.lower() + query_params.append(f"fetchSensitivityLabelMetadata={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def create_worksheet_async( + self, + input: CreateWorksheetInput, + drive: str, + file: str, + source: Optional[str], + ): + """ + Create worksheet + + Create a new worksheet in the Excel workbook. + """ + path = ( + f"{self._connection_runtime_url}" + f"/codeless" + f"/v1.0" + f"/drives" + f"/{str(drive)}" + f"/items" + f"/{str(file)}" + f"/workbook" + f"/worksheets" + ) + query_params = [] + if source is not None: + value = str(source) + if isinstance(source, bool): + value = value.lower() + query_params.append(f"source={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def get_tables_async( + self, + drive: str, + file: str, + source: Optional[str], + select: Optional[str] = None, + extract_sensitivity_label: Optional[str] = None, + fetch_sensitivity_label_metadata: Optional[str] = None, + ): + """ + Get tables + + Get a list of tables in the Excel workbook. + """ + path = ( + f"{self._connection_runtime_url}" + f"/codeless" + f"/v1.0" + f"/drives" + f"/{str(drive)}" + f"/items" + f"/{str(file)}" + f"/workbook" + f"/tables" + ) + query_params = [] + if source is not None: + value = str(source) + if isinstance(source, bool): + value = value.lower() + query_params.append(f"source={quote(value)}") + if select is not None: + value = str(select) + if isinstance(select, bool): + value = value.lower() + query_params.append(f"$select={quote(value)}") + if extract_sensitivity_label is not None: + value = str(extract_sensitivity_label) + if isinstance(extract_sensitivity_label, bool): + value = value.lower() + query_params.append(f"extractSensitivityLabel={quote(value)}") + if fetch_sensitivity_label_metadata is not None: + value = str(fetch_sensitivity_label_metadata) + if isinstance(fetch_sensitivity_label_metadata, bool): + value = value.lower() + query_params.append(f"fetchSensitivityLabelMetadata={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def add_row_async( + self, + input: Item, + drive: str, + file: str, + table: str, + source: Optional[str], + date_time_format: Optional[str] = None, + ): + """ + Add a row into a table + + Add a new row into the Excel table. + """ + path = ( + f"{self._connection_runtime_url}" + f"/codeless" + f"/v1.2" + f"/drives" + f"/{str(drive)}" + f"/items" + f"/{str(file)}" + f"/workbook" + f"/tables" + f"/{str(table)}" + f"/rows" + ) + query_params = [] + if source is not None: + value = str(source) + if isinstance(source, bool): + value = value.lower() + query_params.append(f"source={quote(value)}") + if date_time_format is not None: + value = str(date_time_format) + if isinstance(date_time_format, bool): + value = value.lower() + query_params.append(f"dateTimeFormat={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + async def run_script_prod_async( self, input: RunScriptProdInput, diff --git a/src/azure/connectors/office365.py b/src/azure/connectors/office365.py index f4d1ace..8ac21f7 100644 --- a/src/azure/connectors/office365.py +++ b/src/azure/connectors/office365.py @@ -3183,7 +3183,14 @@ async def update_draft_email_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("PATCH", path, body=input) + response = await self.http_client.send_async("PATCH", path, body=input) + if response.status >= 400: + raise ConnectorException( + "PATCH", + path, + response.status, + response.text, + ) async def send_draft_email_async( self, @@ -3196,7 +3203,14 @@ async def send_draft_email_async( """ path = f"{self._connection_runtime_url}/Draft/Send/{str(message_id)}" - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + if response.status >= 400: + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def assign_category_async( self, @@ -3223,7 +3237,14 @@ async def assign_category_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + if response.status >= 400: + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def assign_category_bulk_async( self, @@ -3339,7 +3360,14 @@ async def update_my_contact_photo_async( f"/$value" ) - await self.http_client.send_async("PUT", path, body=input) + response = await self.http_client.send_async("PUT", path, body=input) + if response.status >= 400: + raise ConnectorException( + "PUT", + path, + response.status, + response.text, + ) async def http_request_async( self, @@ -3491,7 +3519,14 @@ async def calendar_delete_item_async( f"/codeless/v1.0/me/calendars/{str(calendar)}/events/{str(event)}" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + if response.status >= 400: + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def calendar_get_item_async( self, @@ -3867,7 +3902,14 @@ async def contact_delete_item_async( f"/{str(id)}" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + if response.status >= 400: + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def contact_get_item_async( self, @@ -4076,7 +4118,14 @@ async def delete_email_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + if response.status >= 400: + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def export_email_async( self, @@ -4166,7 +4215,14 @@ async def flag_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("PATCH", path, body=input) + response = await self.http_client.send_async("PATCH", path, body=input) + if response.status >= 400: + raise ConnectorException( + "PATCH", + path, + response.status, + response.text, + ) async def forward_email_async( self, @@ -4192,7 +4248,14 @@ async def forward_email_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + if response.status >= 400: + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def get_attachment_async( self, @@ -4624,7 +4687,14 @@ async def mark_as_read_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("PATCH", path, body=input) + response = await self.http_client.send_async("PATCH", path, body=input) + if response.status >= 400: + raise ConnectorException( + "PATCH", + path, + response.status, + response.text, + ) async def move_async( self, @@ -4990,7 +5060,14 @@ async def reply_to_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + if response.status >= 400: + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def respond_to_event_async( self, @@ -5008,7 +5085,14 @@ async def respond_to_event_async( f"/codeless/v1.0/me/events/{str(event_id)}/{str(response)}" ) - await self.http_client.send_async("POST", path, body=input) + http_response = await self.http_client.send_async("POST", path, body=input) + if http_response.status >= 400: + raise ConnectorException( + "POST", + path, + http_response.status, + http_response.text, + ) async def send_email_async( self, @@ -5021,7 +5105,14 @@ async def send_email_async( """ path = f"{self._connection_runtime_url}/v2/Mail" - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + if response.status >= 400: + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def set_automatic_replies_setting_async( self, @@ -5159,4 +5250,11 @@ async def shared_mailbox_send_email_async( """ path = f"{self._connection_runtime_url}/v2/SharedMailbox/Mail" - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + if response.status >= 400: + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) diff --git a/src/azure/connectors/onedrive.py b/src/azure/connectors/onedrive.py index bf1bd8a..b98e911 100644 --- a/src/azure/connectors/onedrive.py +++ b/src/azure/connectors/onedrive.py @@ -254,7 +254,15 @@ async def delete_file_async( f"{self._connection_runtime_url}/datasets/default/files/{str(id)}" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def get_file_metadata_by_path_async( self, @@ -328,7 +336,7 @@ async def get_file_content_by_path_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def get_file_content_async( self, @@ -363,7 +371,7 @@ async def get_file_content_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def create_file_async( self, @@ -670,7 +678,7 @@ async def convert_file_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def convert_file_by_path_async( self, @@ -712,7 +720,7 @@ async def convert_file_by_path_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def get_file_tags_async( self, @@ -804,7 +812,15 @@ async def remove_file_tag_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def get_file_thumbnail_async( self, @@ -1250,7 +1266,7 @@ async def on_new_file_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def on_new_files_async( self, @@ -1366,7 +1382,7 @@ async def on_updated_file_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def on_updated_files_async( self, diff --git a/src/azure/connectors/outlook.py b/src/azure/connectors/outlook.py index 1e04e17..69c8be7 100644 --- a/src/azure/connectors/outlook.py +++ b/src/azure/connectors/outlook.py @@ -1534,7 +1534,15 @@ async def delete_email_async( """ path = f"{self._connection_runtime_url}/Mail/{str(message_id)}" - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def move_async( self, @@ -1582,7 +1590,15 @@ async def flag_async( """ path = f"{self._connection_runtime_url}/Mail/Flag/{str(message_id)}" - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def mark_as_read_async( self, @@ -1597,7 +1613,15 @@ async def mark_as_read_async( f"{self._connection_runtime_url}/Mail/MarkAsRead/{str(message_id)}" ) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def get_attachment_async( self, @@ -1624,7 +1648,7 @@ async def get_attachment_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def send_mail_with_options_async( self, @@ -1725,7 +1749,15 @@ async def calendar_delete_item_async( f"/datasets/calendars/tables/{str(table)}/items/{str(id)}" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def contact_get_tables_async( self, @@ -1883,7 +1915,15 @@ async def contact_delete_item_async( f"/datasets/contacts/tables/{str(table)}/items/{str(id)}" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def contact_patch_item_async( self, @@ -1932,7 +1972,15 @@ async def respond_to_event_async( f"/codeless/api/v2.0/me/events/{str(event_id)}/{str(response)}" ) - await self.http_client.send_async("POST", path, body=input) + http_response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= http_response.status < 300): + raise ConnectorException( + "POST", + path, + http_response.status, + http_response.text, + ) async def forward_email_async( self, @@ -1949,7 +1997,15 @@ async def forward_email_async( f"/codeless/api/v2.0/me/messages/{str(message_id)}/forward" ) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def calendar_get_item_async( self, @@ -2762,7 +2818,15 @@ async def reply_to_async( f"{self._connection_runtime_url}/v3/Mail/ReplyTo/{str(message_id)}" ) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def send_email_async( self, @@ -2775,4 +2839,12 @@ async def send_email_async( """ path = f"{self._connection_runtime_url}/v2/Mail" - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) diff --git a/src/azure/connectors/servicebus.py b/src/azure/connectors/servicebus.py index 06537d2..f71d2f1 100644 --- a/src/azure/connectors/servicebus.py +++ b/src/azure/connectors/servicebus.py @@ -201,7 +201,15 @@ async def send_message_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def send_messages_async( self, @@ -226,7 +234,15 @@ async def send_messages_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def get_message_from_queue_async( self, @@ -346,7 +362,15 @@ async def complete_message_in_queue_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def abandon_message_in_queue_async( self, @@ -383,7 +407,15 @@ async def abandon_message_in_queue_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def get_deferred_message_from_queue_async( self, @@ -468,7 +500,15 @@ async def defer_message_in_queue_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def dead_letter_message_in_queue_async( self, @@ -511,7 +551,15 @@ async def dead_letter_message_in_queue_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def renew_lock_on_message_in_queue_async( self, @@ -542,7 +590,15 @@ async def renew_lock_on_message_in_queue_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def get_messages_from_queue_async( self, @@ -707,7 +763,15 @@ async def close_session_in_queue_async( f"/{str(queue_name)}/sessions/{str(session_id)}/close" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def renew_lock_on_session_in_queue_async( self, @@ -724,7 +788,15 @@ async def renew_lock_on_session_in_queue_async( f"/{str(queue_name)}/sessions/{str(session_id)}/renewlock" ) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def get_message_from_topic_async( self, @@ -861,7 +933,15 @@ async def complete_message_in_topic_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def abandon_message_in_topic_async( self, @@ -903,7 +983,15 @@ async def abandon_message_in_topic_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def get_deferred_message_from_topic_async( self, @@ -1000,7 +1088,15 @@ async def defer_message_in_topic_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def dead_letter_message_in_topic_async( self, @@ -1048,7 +1144,15 @@ async def dead_letter_message_in_topic_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def renew_lock_on_message_in_topic_async( self, @@ -1084,7 +1188,15 @@ async def renew_lock_on_message_in_topic_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def create_topic_subscription_async( self, @@ -1141,7 +1253,15 @@ async def delete_topic_subscription_async( f"/{str(topic_name)}/subscriptions/{str(subscription_name)}" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def get_messages_from_topic_async( self, @@ -1332,7 +1452,15 @@ async def close_session_in_topic_async( f"/close" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def renew_lock_on_session_in_topic_async( self, @@ -1355,4 +1483,12 @@ async def renew_lock_on_session_in_topic_async( f"/renewlock" ) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) diff --git a/src/azure/connectors/sharepointonline.py b/src/azure/connectors/sharepointonline.py index d5c130b..8c72f43 100644 --- a/src/azure/connectors/sharepointonline.py +++ b/src/azure/connectors/sharepointonline.py @@ -22,32 +22,30 @@ # Type Definitions @dataclass -class TablesList: - """Response for Get all lists and libraries""" - - value: Optional[List[Table]] = None - """List of Tables""" - - -@dataclass -class ApproveHubSiteJoinResponse: - """Response for Approve hub site join request""" - - approval_token: Optional[str] = None - """Approval Token""" - - -@dataclass -class SharingLinkPermission: - """Response for Create sharing link for a file or folder""" +class CreateAgreementsSolutionDocumentInput: + """ + Agreements Solution - Generate document within Agreements Solution + workspace + """ - link: Optional[SharingLink] = None + additional_properties: Dict[str, Any] = field(default_factory=dict) + """ + Dynamic properties determined at runtime + (similar to .NET [JsonExtensionData]) + """ @dataclass -class BlobMetadata: - """Response for Copy file (deprecated)""" +class SPBlobMetadataResponse: + """ + Response for Agreements Solution - Generate document within Agreements + Solution workspace + """ + item_id: Optional[int] = None + """ + The value that can be used to Get or Update file properties in libraries. + """ id: Optional[str] = None """The unique id of the file or folder.""" name: Optional[str] = None @@ -74,13 +72,32 @@ class BlobMetadata: @dataclass -class SPBlobMetadataResponse: - """Response for Copy file""" +class TablesList: + """Response for Get all lists and libraries""" + + value: Optional[List[Table]] = None + """List of Tables""" + + +@dataclass +class ApproveHubSiteJoinResponse: + """Response for Approve hub site join request""" + + approval_token: Optional[str] = None + """Approval Token""" + + +@dataclass +class SharingLinkPermission: + """Response for Create sharing link for a file or folder""" + + link: Optional[SharingLink] = None + + +@dataclass +class BlobMetadata: + """Response for Copy file (deprecated)""" - item_id: Optional[int] = None - """ - The value that can be used to Get or Update file properties in libraries. - """ id: Optional[str] = None """The unique id of the file or folder.""" name: Optional[str] = None @@ -198,6 +215,46 @@ class SPListExpandedUser: type_: Optional[str] = None +@dataclass +class TableForm: + """Response for Get form metadata (preview)""" + + form_id: Optional[str] = None + """Gets or sets the form ID""" + display_name: Optional[str] = None + """Gets or sets the form display name""" + type_: Optional[str] = None + """Gets or sets the form type""" + link: Optional[str] = None + """Gets or sets the form link/URL""" + created_by: Optional[str] = None + """Gets or sets the user who created the form""" + created: Optional[str] = None + """Gets or sets the date the form was created""" + modified: Optional[str] = None + """Gets or sets the date the form was last modified""" + modified_by: Optional[str] = None + """Gets or sets the user who last modified the form""" + output_format: Optional[str] = None + """ + Gets or sets the output format (e.g. \"Word document\", \"PDF document\", + \"None\") + """ + fields_metadata: Optional[List[FormFieldMetadata]] = None + """Gets or sets Form Fields""" + + +@dataclass +class SubmitDocGenFormInput: + """Generate a document from a form (preview)""" + + additional_properties: Dict[str, Any] = field(default_factory=dict) + """ + Dynamic properties determined at runtime + (similar to .NET [JsonExtensionData]) + """ + + @dataclass class ItemsList: """Response for Get files (properties only)""" @@ -390,20 +447,6 @@ class Table: """Additional table properties provided by the connector to the clients.""" -@dataclass -class CreateAgreementsSolutionDocumentInput: - """ - Agreements Solution - Generate document within Agreements Solution - workspace - """ - - additional_properties: Dict[str, Any] = field(default_factory=dict) - """ - Dynamic properties determined at runtime - (similar to .NET [JsonExtensionData]) - """ - - @dataclass class ApplicationPermissionIdentity: """Definition: ApplicationPermissionIdentity""" @@ -541,6 +584,22 @@ class FileCheckInParameters: """Select the type of version you would like to check in""" +@dataclass +class FormFieldMetadata: + """Definition: FormFieldMetadata""" + + id: Optional[str] = None + """Gets or sets the field ID""" + name: Optional[str] = None + """Gets or sets the field Name""" + is_required: Optional[bool] = None + """Gets or sets a value indicating whether the field is required""" + data_type: Optional[str] = None + """Gets or sets the field data type""" + default_value: Optional[str] = None + """Gets or sets the default value""" + + @dataclass class GetItemChangesMetadataResponse: """Definition: GetItemChangesMetadataResponse""" @@ -897,6 +956,7 @@ class TableCapabilitiesMetadata: sort_restrictions: Optional[TableSortRestrictionsMetadata] = None filter_restrictions: Optional[TableFilterRestrictionsMetadata] = None select_restrictions: Optional[TableSelectRestrictionsMetadata] = None + count_restrictions: Optional[TableCountRestrictionsMetadata] = None is_only_server_pagable: Optional[bool] = None """Server paging restrictions""" filter_function_support: Optional[List[str]] = None @@ -905,6 +965,14 @@ class TableCapabilitiesMetadata: """List of supported server-driven paging capabilities""" +@dataclass +class TableCountRestrictionsMetadata: + """Definition: TableCountRestrictionsMetadata""" + + countable: Optional[bool] = None + """Indicates whether this table has countable columns""" + + @dataclass class TableFilterRestrictionsMetadata: """Definition: TableFilterRestrictionsMetadata""" @@ -931,7 +999,7 @@ class TableMetadata: schema: Optional[ObjectEntity] = None referenced_entities: Optional[ObjectEntity] = None web_url: Optional[str] = None - """Url link""" + """URL link""" @dataclass @@ -1014,6 +1082,54 @@ def __init__( def connector_name(self) -> str: return "sharepointonline" + async def create_agreements_solution_document_async( + self, + input: CreateAgreementsSolutionDocumentInput, + dataset: str, + template: str, + document_name: Optional[str] = None, + ): + """ + Agreements Solution - Generate document within Agreements Solution + workspace + + Use this action to create documents based on modern templates in a + Agreements Solution workspace. This is behind a payment wall currently + in planning (either license or PayG). + """ + path = ( + f"{self._connection_runtime_url}" + f"/datasets" + f"/{quote(str(dataset), safe='')}" + f"/agreements" + f"/templates" + f"/{str(template)}" + f"/createnewdocument" + ) + query_params = [] + if document_name is not None: + value = str(document_name) + if isinstance(document_name, bool): + value = value.lower() + query_params.append(f"documentName={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + async def get_all_tables_async( self, dataset: str, @@ -1110,7 +1226,15 @@ async def cancel_hub_site_join_approval_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def create_sharing_link_async( self, @@ -1250,6 +1374,7 @@ async def create_file_async( dataset: str, folder_path: Optional[str], name: Optional[str], + overwrite: Optional[str] = None, query_parameters_single_encoded: Optional[str] = None, ): """ @@ -1273,6 +1398,11 @@ async def create_file_async( if isinstance(name, bool): value = value.lower() query_params.append(f"name={quote(value)}") + if overwrite is not None: + value = str(overwrite) + if isinstance(overwrite, bool): + value = value.lower() + query_params.append(f"overwrite={quote(value)}") if query_parameters_single_encoded is not None: value = str(query_parameters_single_encoded) if isinstance(query_parameters_single_encoded, bool): @@ -1374,7 +1504,15 @@ async def delete_file_async( f"/datasets/{quote(str(dataset), safe='')}/files/{str(id)}" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def get_file_content_async( self, @@ -1411,7 +1549,7 @@ async def get_file_content_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def get_file_metadata_by_path_async( self, @@ -1504,7 +1642,7 @@ async def get_file_content_by_path_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def get_folder_metadata_async( self, @@ -1607,7 +1745,15 @@ async def http_request_async( f"/datasets/{quote(str(dataset), safe='')}/httprequest" ) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def join_hub_site_async( self, @@ -1647,7 +1793,15 @@ async def join_hub_site_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def move_file_async( self, @@ -1741,7 +1895,15 @@ async def notify_hub_site_join_approval_started_async( if query_params: path += '?' + '&'.join(query_params) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def get_tables_async( self, @@ -1905,6 +2067,98 @@ async def search_for_user_async( return json.loads(response.text) + async def get_table_form_async( + self, + dataset: str, + table: str, + form: str, + ): + """ + Get form metadata (preview) + + Use this action to get the form metadata, which includes the form ID, + title, link, form type, and the output format. It also gets the form + questions used to collect information. Document generation forms is a + part of AI in SharePoint Public Preview. For more info on getting + started, see: + https://learn.microsoft.com/sharepoint/dev/declarative-customization/structured-documents. + """ + path = ( + f"{self._connection_runtime_url}" + f"/datasets" + f"/{quote(str(dataset), safe='')}" + f"/tables" + f"/{str(table)}" + f"/forms" + f"/{str(form)}" + ) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def submit_doc_gen_form_async( + self, + input: SubmitDocGenFormInput, + dataset: str, + table: str, + form: str, + view: Optional[str] = None, + ): + """ + Generate a document from a form (preview) + + Use this action to create documents from a document generation + template. Map the template fields to the corresponding content in the + data source. Document generation forms is a part of AI in SharePoint + Public Preview. For more info on getting started, see: + https://learn.microsoft.com/sharepoint/dev/declarative-customization/structured-documents. + """ + path = ( + f"{self._connection_runtime_url}" + f"/datasets" + f"/{quote(str(dataset), safe='')}" + f"/tables" + f"/{str(table)}" + f"/forms" + f"/{str(form)}" + f"/submitdocgenform" + ) + query_params = [] + if view is not None: + value = str(view) + if isinstance(view, bool): + value = value.lower() + query_params.append(f"view={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + async def get_file_items_async( self, dataset: str, @@ -2166,7 +2420,15 @@ async def delete_item_async( f"/{str(id)}" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def patch_item_async( self, @@ -2350,7 +2612,15 @@ async def check_in_file_async( f"/checkinfile" ) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def check_out_file_async( self, @@ -2376,7 +2646,15 @@ async def check_out_file_async( f"/checkoutfile" ) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def discard_file_check_out_async( self, @@ -2405,7 +2683,15 @@ async def discard_file_check_out_async( f"/discardfilecheckout" ) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def get_file_item_async( self, @@ -2481,7 +2767,15 @@ async def grant_access_async( f"/grantaccess" ) - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def patch_file_item_async( self, @@ -2656,7 +2950,15 @@ async def unshare_item_async( f"/unshare" ) - await self.http_client.send_async("POST", path, body=None) + response = await self.http_client.send_async("POST", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) async def get_item_attachments_async( self, @@ -2768,7 +3070,15 @@ async def delete_attachment_async( f"/{str(attachment_id)}" ) - await self.http_client.send_async("DELETE", path, body=None) + response = await self.http_client.send_async("DELETE", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "DELETE", + path, + response.status, + response.text, + ) async def get_attachment_content_async( self, @@ -2806,7 +3116,7 @@ async def get_attachment_content_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def get_on_changed_items_async( self, @@ -3035,6 +3345,131 @@ async def get_on_new_items_async( return json.loads(response.text) + async def get_on_new_items_from_form_async( + self, + dataset: str, + table: str, + form: Optional[str], + view: Optional[str] = None, + ): + """ + When a form is submitted (preview) + + This operation triggers a flow when a list, document generation, or + file upload form is submitted. Document generation forms is a part of + AI in SharePoint Public Preview. For more info on getting started, + see:https://learn.microsoft.com/en-us/sharepoint/ai-in-sharepoint-structured-document-generation + """ + path = ( + f"{self._connection_runtime_url}" + f"/datasets" + f"/{quote(str(dataset), safe='')}" + f"/tables" + f"/{str(table)}" + f"/onnewitemsfromform" + ) + query_params = [] + if form is not None: + value = str(form) + if isinstance(form, bool): + value = value.lower() + query_params.append(f"form={quote(value)}") + if view is not None: + value = str(view) + if isinstance(view, bool): + value = value.lower() + query_params.append(f"view={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + + async def get_on_recurrence_digest_async( + self, + dataset: str, + table: str, + update: Optional[str], + add: Optional[str], + run_schedule: Optional[str], + folder_path: Optional[str] = None, + view: Optional[str] = None, + start_time: Optional[str] = None, + ): + """ + Recurring digest of updates (preview) + + Recurring digest of updates for a list or library. + """ + path = ( + f"{self._connection_runtime_url}" + f"/datasets" + f"/{quote(str(dataset), safe='')}" + f"/tables" + f"/{str(table)}" + f"/onrecurrencedigest" + ) + query_params = [] + if update is not None: + value = str(update) + if isinstance(update, bool): + value = value.lower() + query_params.append(f"update={quote(value)}") + if add is not None: + value = str(add) + if isinstance(add, bool): + value = value.lower() + query_params.append(f"add={quote(value)}") + if run_schedule is not None: + value = str(run_schedule) + if isinstance(run_schedule, bool): + value = value.lower() + query_params.append(f"runSchedule={quote(value)}") + if folder_path is not None: + value = str(folder_path) + if isinstance(folder_path, bool): + value = value.lower() + query_params.append(f"folderPath={quote(value)}") + if view is not None: + value = str(view) + if isinstance(view, bool): + value = value.lower() + query_params.append(f"view={quote(value)}") + if start_time is not None: + value = str(start_time) + if isinstance(start_time, bool): + value = value.lower() + query_params.append(f"startTime={quote(value)}") + if query_params: + path += '?' + '&'.join(query_params) + + response = await self.http_client.send_async("GET", path, body=None) + + if not (200 <= response.status < 300): + raise ConnectorException( + "GET", + path, + response.status, + response.text, + ) + + if not response.text: + return None + + return json.loads(response.text) + async def get_on_updated_file_classified_times_async( self, dataset: str, @@ -3279,66 +3714,6 @@ async def get_table_views_async( return json.loads(response.text) - async def create_agreements_solution_document_async( - self, - input: CreateAgreementsSolutionDocumentInput, - dataset: str, - template: str, - document_name: Optional[str] = None, - table: Optional[str] = None, - view: Optional[str] = None, - ): - """ - Agreements Solution - Generate document within Agreements Solution - workspace - - Use this action to create documents based on modern templates in a - Agreements Solution workspace. This is behind a payment wall currently - in planning (either license or PayG). - """ - path = ( - f"{self._connection_runtime_url}" - f"/datasets" - f"/{quote(str(dataset), safe='')}" - f"/agreements" - f"/templates" - f"/{str(template)}" - f"/createnewdocument" - ) - query_params = [] - if document_name is not None: - value = str(document_name) - if isinstance(document_name, bool): - value = value.lower() - query_params.append(f"documentName={quote(value)}") - if table is not None: - value = str(table) - if isinstance(table, bool): - value = value.lower() - query_params.append(f"table={quote(value)}") - if view is not None: - value = str(view) - if isinstance(view, bool): - value = value.lower() - query_params.append(f"view={quote(value)}") - if query_params: - path += '?' + '&'.join(query_params) - - response = await self.http_client.send_async("POST", path, body=input) - - if not (200 <= response.status < 300): - raise ConnectorException( - "POST", - path, - response.status, - response.text, - ) - - if not response.text: - return None - - return json.loads(response.text) - async def on_new_file_async( self, dataset: str, @@ -3386,7 +3761,7 @@ async def on_new_file_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def on_updated_file_async( self, @@ -3442,7 +3817,7 @@ async def on_updated_file_async( response.text, ) - return response.text.encode('latin-1') if response.text else b'' + return response.content async def extract_folder_async( self, diff --git a/src/azure/connectors/smtp.py b/src/azure/connectors/smtp.py index 07bd637..c0b0fb0 100644 --- a/src/azure/connectors/smtp.py +++ b/src/azure/connectors/smtp.py @@ -13,6 +13,7 @@ ConnectorClientOptions, TokenProvider, ManagedIdentityTokenProvider, + ConnectorException, ) @@ -202,4 +203,12 @@ async def send_email_async( """ path = f"{self._connection_runtime_url}/SendEmailV3" - await self.http_client.send_async("POST", path, body=input) + response = await self.http_client.send_async("POST", path, body=input) + + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) diff --git a/src/azure/connectors/teams.py b/src/azure/connectors/teams.py index 7b8875b..acf759e 100644 --- a/src/azure/connectors/teams.py +++ b/src/azure/connectors/teams.py @@ -2725,7 +2725,7 @@ async def webhook_chat_message_trigger_async( f"/beta/subscriptions/chatmessagetrigger" ) - response = await self.http_client.send_async("POST", path, body=input) + response = response = await self.http_client.send_async("POST", path, body=input) if not (200 <= response.status < 300): raise ConnectorException( @@ -2800,6 +2800,14 @@ async def webhook_new_message_trigger_async( response.text, ) + if not (200 <= response.status < 300): + raise ConnectorException( + "POST", + path, + response.status, + response.text, + ) + async def subscribe_user_message_with_options_async( self, input: DynamicUserMessageWithOptionsSubscriptionRequest, diff --git a/tests/test_arm.py b/tests/test_arm.py index f3cdb46..112dd41 100644 --- a/tests/test_arm.py +++ b/tests/test_arm.py @@ -49,6 +49,10 @@ from tests.conftest import MockResponse +# Default API version for tests +DEFAULT_API_VERSION = "2021-04-01" + + class TestArmClientInitialization: """Tests for ArmClient initialization.""" @@ -176,11 +180,14 @@ async def test_success_with_subscriptions(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ) as mock_send: - result = await client.subscriptions_list_async() + result = await client.subscriptions_list_async( + x_ms_api_version=DEFAULT_API_VERSION + ) mock_send.assert_called_once_with( "GET", - "https://example.azure.com/connections/test/subscriptions", + "https://example.azure.com/connections/test/subscriptions" + f"?x-ms-api-version={DEFAULT_API_VERSION}", body=None ) assert result is not None @@ -189,6 +196,32 @@ async def test_success_with_subscriptions(self, mock_token_provider): assert result["value"][0]["displayName"] == "Production Subscription" assert result["value"][1]["displayName"] == "Development Subscription" + @pytest.mark.asyncio + async def test_success_without_api_version(self, mock_token_provider): + """Test GET request without api version omits query parameter.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response_data = {"value": [], "nextLink": None} + mock_response = MockResponse(status=200, text=json.dumps(mock_response_data)) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.subscriptions_list_async(x_ms_api_version=None) + + mock_send.assert_called_once_with( + "GET", + "https://example.azure.com/connections/test/subscriptions", + body=None + ) + assert result is not None + @pytest.mark.asyncio async def test_success_with_empty_subscriptions(self, mock_token_provider): """Test successful GET request with no subscriptions.""" @@ -206,7 +239,9 @@ async def test_success_with_empty_subscriptions(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ) as mock_send: - result = await client.subscriptions_list_async() + result = await client.subscriptions_list_async( + x_ms_api_version=DEFAULT_API_VERSION + ) mock_send.assert_called_once() assert result is not None @@ -228,7 +263,9 @@ async def test_success_with_empty_response_body(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ) as mock_send: - result = await client.subscriptions_list_async() + result = await client.subscriptions_list_async( + x_ms_api_version=DEFAULT_API_VERSION + ) mock_send.assert_called_once() assert result is None @@ -260,7 +297,9 @@ async def test_success_with_pagination(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ): - result = await client.subscriptions_list_async() + result = await client.subscriptions_list_async( + x_ms_api_version=DEFAULT_API_VERSION + ) assert result is not None assert "nextLink" in result @@ -286,7 +325,9 @@ async def test_error_unauthorized(self, mock_token_provider): return_value=mock_response ): with pytest.raises(ConnectorException) as exc_info: - await client.subscriptions_list_async() + await client.subscriptions_list_async( + x_ms_api_version=DEFAULT_API_VERSION + ) assert exc_info.value.status_code == 401 @@ -310,7 +351,9 @@ async def test_error_forbidden(self, mock_token_provider): return_value=mock_response ): with pytest.raises(ConnectorException) as exc_info: - await client.subscriptions_list_async() + await client.subscriptions_list_async( + x_ms_api_version=DEFAULT_API_VERSION + ) assert exc_info.value.status_code == 403 @@ -334,7 +377,9 @@ async def test_error_not_found(self, mock_token_provider): return_value=mock_response ): with pytest.raises(ConnectorException) as exc_info: - await client.subscriptions_list_async() + await client.subscriptions_list_async( + x_ms_api_version=DEFAULT_API_VERSION + ) assert exc_info.value.status_code == 404 @@ -358,7 +403,9 @@ async def test_error_server_error(self, mock_token_provider): return_value=mock_response ): with pytest.raises(ConnectorException) as exc_info: - await client.subscriptions_list_async() + await client.subscriptions_list_async( + x_ms_api_version=DEFAULT_API_VERSION + ) assert exc_info.value.status_code == 500 @@ -744,3 +791,594 @@ def test_resource_management_error_creation(self): assert error.target == "properties.location" assert len(error.details) == 1 assert error.details[0]["code"] == "InvalidLocation" + + +class TestResourceGroupsListAsync: + """Tests for resource_groups_list_async method.""" + + @pytest.mark.asyncio + async def test_success_with_resource_groups(self, mock_token_provider): + """Test successful GET request returns resource groups.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response_data = { + "value": [ + { + "id": "/subscriptions/sub-1/resourceGroups/rg-1", + "name": "rg-1", + "location": "westus", + "properties": {"provisioningState": "Succeeded"} + }, + { + "id": "/subscriptions/sub-1/resourceGroups/rg-2", + "name": "rg-2", + "location": "eastus", + "properties": {"provisioningState": "Succeeded"} + } + ], + "nextLink": None + } + mock_response = MockResponse(status=200, text=json.dumps(mock_response_data)) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.resource_groups_list_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1" + ) + + mock_send.assert_called_once_with( + "GET", + "https://example.azure.com/connections/test/subscriptions/" + f"sub-1/resourcegroups?x-ms-api-version={DEFAULT_API_VERSION}", + body=None + ) + assert result is not None + assert len(result["value"]) == 2 + assert result["value"][0]["name"] == "rg-1" + + @pytest.mark.asyncio + async def test_error_not_found(self, mock_token_provider): + """Test 404 Not Found raises ConnectorException.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=404, + text='{"error": {"code": "NotFound", "message": "Subscription not found"}}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.resource_groups_list_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1" + ) + + assert exc_info.value.status_code == 404 + + +class TestResourceGroupsGetAsync: + """Tests for resource_groups_get_async method.""" + + @pytest.mark.asyncio + async def test_success(self, mock_token_provider): + """Test successful GET request returns resource group.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response_data = { + "id": "/subscriptions/sub-1/resourceGroups/rg-1", + "name": "rg-1", + "location": "westus", + "tags": {"environment": "production"}, + "properties": {"provisioningState": "Succeeded"} + } + mock_response = MockResponse(status=200, text=json.dumps(mock_response_data)) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.resource_groups_get_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1", + resource_group_name="rg-1" + ) + + mock_send.assert_called_once() + assert result is not None + assert result["name"] == "rg-1" + assert result["location"] == "westus" + + +class TestResourceGroupsDeleteAsync: + """Tests for resource_groups_delete_async method (void operation).""" + + @pytest.mark.asyncio + async def test_success_returns_none(self, mock_token_provider): + """Test successful DELETE request returns None.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.resource_groups_delete_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1", + resource_group_name="rg-1" + ) + + mock_send.assert_called_once_with( + "DELETE", + "https://example.azure.com/connections/test/subscriptions/" + f"sub-1/resourcegroups/rg-1?x-ms-api-version={DEFAULT_API_VERSION}", + body=None + ) + assert result is None + + @pytest.mark.asyncio + async def test_success_202_accepted(self, mock_token_provider): + """Test 202 Accepted for async delete returns None.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=202, text="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.resource_groups_delete_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1", + resource_group_name="rg-1" + ) + + assert result is None + + @pytest.mark.asyncio + async def test_error_not_found(self, mock_token_provider): + """Test 404 Not Found raises ConnectorException.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=404, + text='{"error": {"code": "NotFound", "message": "Resource group not found"}}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.resource_groups_delete_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1", + resource_group_name="rg-1" + ) + + assert exc_info.value.status_code == 404 + + +class TestDeploymentsGetAsync: + """Tests for deployments_get_async method.""" + + @pytest.mark.asyncio + async def test_success(self, mock_token_provider): + """Test successful GET request returns deployment.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response_data = { + "id": ( + "/subscriptions/sub-1/resourceGroups/rg-1" + "/providers/Microsoft.Resources/deployments/deploy-1" + ), + "name": "deploy-1", + "properties": { + "provisioningState": "Succeeded", + "mode": "Incremental" + } + } + mock_response = MockResponse(status=200, text=json.dumps(mock_response_data)) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.deployments_get_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1", + resource_group_name="rg-1", + deployment_name="deploy-1" + ) + + mock_send.assert_called_once() + assert result is not None + assert result["name"] == "deploy-1" + + +class TestDeploymentsDeleteAsync: + """Tests for deployments_delete_async method (void operation).""" + + @pytest.mark.asyncio + async def test_success_returns_none(self, mock_token_provider): + """Test successful DELETE request returns None.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.deployments_delete_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1", + resource_group_name="rg-1", + deployment_name="deploy-1" + ) + + assert result is None + + +class TestDeploymentsCancelAsync: + """Tests for deployments_cancel_async method (void operation).""" + + @pytest.mark.asyncio + async def test_success_returns_none(self, mock_token_provider): + """Test successful POST cancel request returns None.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=204, text="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.deployments_cancel_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1", + resource_group_name="rg-1", + deployment_name="deploy-1" + ) + + mock_send.assert_called_once_with( + "POST", + "https://example.azure.com/connections/test/subscriptions/" + "sub-1/resourcegroups/rg-1/providers/Microsoft.Resources/" + f"deployments/deploy-1/cancel?x-ms-api-version={DEFAULT_API_VERSION}", + body=None + ) + assert result is None + + +class TestProvidersListAsync: + """Tests for providers_list_async method.""" + + @pytest.mark.asyncio + async def test_success_with_providers(self, mock_token_provider): + """Test successful GET request returns providers.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response_data = { + "value": [ + { + "namespace": "Microsoft.Compute", + "registrationState": "Registered" + }, + { + "namespace": "Microsoft.Storage", + "registrationState": "Registered" + } + ], + "nextLink": None + } + mock_response = MockResponse(status=200, text=json.dumps(mock_response_data)) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.providers_list_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1" + ) + + mock_send.assert_called_once() + assert result is not None + assert len(result["value"]) == 2 + + +class TestProvidersRegisterAsync: + """Tests for providers_register_async method.""" + + @pytest.mark.asyncio + async def test_success(self, mock_token_provider): + """Test successful POST request registers provider.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response_data = { + "namespace": "Microsoft.Compute", + "registrationState": "Registered" + } + mock_response = MockResponse(status=200, text=json.dumps(mock_response_data)) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.providers_register_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1", + resource_provider_namespace="Microsoft.Compute" + ) + + mock_send.assert_called_once() + assert result is not None + assert result["namespace"] == "Microsoft.Compute" + + +class TestSubscriptionsListLocationsAsync: + """Tests for subscriptions_list_locations_async method.""" + + @pytest.mark.asyncio + async def test_success_with_locations(self, mock_token_provider): + """Test successful GET request returns locations.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response_data = { + "value": [ + { + "id": "/subscriptions/sub-1/locations/westus", + "name": "westus", + "displayName": "West US" + }, + { + "id": "/subscriptions/sub-1/locations/eastus", + "name": "eastus", + "displayName": "East US" + } + ] + } + mock_response = MockResponse(status=200, text=json.dumps(mock_response_data)) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.subscriptions_list_locations_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1" + ) + + mock_send.assert_called_once_with( + "GET", + "https://example.azure.com/connections/test/subscriptions/" + f"sub-1/locations?x-ms-api-version={DEFAULT_API_VERSION}", + body=None + ) + assert result is not None + assert len(result["value"]) == 2 + + +class TestSubscriptionsGetAsync: + """Tests for subscriptions_get_async method.""" + + @pytest.mark.asyncio + async def test_success(self, mock_token_provider): + """Test successful GET request returns subscription.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response_data = { + "id": "/subscriptions/sub-1", + "subscriptionId": "sub-1", + "displayName": "Production Subscription", + "state": "Enabled" + } + mock_response = MockResponse(status=200, text=json.dumps(mock_response_data)) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.subscriptions_get_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1" + ) + + mock_send.assert_called_once_with( + "GET", + "https://example.azure.com/connections/test/subscriptions/" + f"sub-1?x-ms-api-version={DEFAULT_API_VERSION}", + body=None + ) + assert result is not None + assert result["subscriptionId"] == "sub-1" + + +class TestTagsDeleteValueAsync: + """Tests for tags_delete_value_async method (void operation).""" + + @pytest.mark.asyncio + async def test_success_returns_none(self, mock_token_provider): + """Test successful DELETE request returns None.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.tags_delete_value_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1", + tag_name="environment", + tag_value="deprecated" + ) + + assert result is None + + +class TestTagsDeleteAsync: + """Tests for tags_delete_async method (void operation).""" + + @pytest.mark.asyncio + async def test_success_returns_none(self, mock_token_provider): + """Test successful DELETE request returns None.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.tags_delete_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1", + tag_name="obsolete-tag" + ) + + assert result is None + + +class TestResourcesDeleteByIdAsync: + """Tests for resources_delete_by_id_async method (void operation).""" + + @pytest.mark.asyncio + async def test_success_returns_none(self, mock_token_provider): + """Test successful DELETE request returns None.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.resources_delete_by_id_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1", + resource_group_name="rg-1", + resource_provider_namespace="Microsoft.Compute", + short_resource_id="virtualMachines/vm-1" + ) + + assert result is None + + @pytest.mark.asyncio + async def test_error_not_found(self, mock_token_provider): + """Test 404 Not Found raises ConnectorException.""" + client = ArmClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=404, + text='{"error": {"code": "NotFound", "message": "Resource not found"}}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.resources_delete_by_id_async( + x_ms_api_version=DEFAULT_API_VERSION, + subscription_id="sub-1", + resource_group_name="rg-1", + resource_provider_namespace="Microsoft.Compute", + short_resource_id="virtualMachines/vm-1" + ) + + assert exc_info.value.status_code == 404 diff --git a/tests/test_azuread.py b/tests/test_azuread.py index 3c9d308..c291984 100644 --- a/tests/test_azuread.py +++ b/tests/test_azuread.py @@ -14,6 +14,11 @@ GetGroupResponse, GetUserResponse, GetGroupMembersResponse, + UpdateUserRequest, + GetGroupRequest, + AssignManagerRequest, + CheckMemberGroupsRequest, + GetMemberGroupsRequest, ) from azure.connectors.sdk import ( ConnectorClientOptions, @@ -449,6 +454,30 @@ async def test_with_different_ids(self, mock_token_provider): assert "5e6cf5c7-b511-4842-6aae-3f6b8ae5e95b" in call_args[0][1] assert "8a9bf2d1-c322-5933-7bbf-4g7c9bf6f06c" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Member not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.remove_member_from_group_async( + group_id="group-abc123", + member_id="nonexistent-user" + ) + + assert exc_info.value.status_code == 404 + class TestCreateGroup: """Tests for create_group_async method.""" @@ -726,3 +755,716 @@ async def test_multiple_consecutive_calls(self, mock_token_provider): await client.create_group_async(input=group_input2) assert mock_send.call_count == 2 + + +class TestGetGroup: + """Tests for get_group_async method.""" + + @pytest.mark.asyncio + async def test_success_with_json_response(self, mock_token_provider): + """Test successful GET request returns group.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"id": "group-123", "displayName": "Engineering", "mailEnabled": true}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.get_group_async(id="group-123") + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "GET" + assert "/v1.0/groups/group-123" in call_args[0][1] + assert result["id"] == "group-123" + assert result["displayName"] == "Engineering" + + @pytest.mark.asyncio + async def test_empty_response_returns_none(self, mock_token_provider): + """Test that empty response returns None.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_group_async(id="group-123") + assert result is None + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Group not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_group_async(id="nonexistent-group") + + assert exc_info.value.status_code == 404 + + +class TestGetUser: + """Tests for get_user_async method.""" + + @pytest.mark.asyncio + async def test_success_with_json_response(self, mock_token_provider): + """Test successful GET request returns user.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text=( + '{"id": "user-456", "displayName": "John Doe", ' + '"mail": "john.doe@contoso.com", "jobTitle": "Engineer"}' + ) + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.get_user_async(id="user-456") + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "GET" + assert "/v1.0/users/user-456" in call_args[0][1] + assert result["id"] == "user-456" + assert result["displayName"] == "John Doe" + + @pytest.mark.asyncio + async def test_empty_response_returns_none(self, mock_token_provider): + """Test that empty response returns None.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_user_async(id="user-456") + assert result is None + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "User not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_user_async(id="nonexistent-user") + + assert exc_info.value.status_code == 404 + + +class TestUpdateUser: + """Tests for update_user_async method (void operation).""" + + @pytest.mark.asyncio + async def test_success(self, mock_token_provider): + """Test successful PATCH request.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=204, text="") + user_input = UpdateUserRequest( + display_name="John Updated", + job_title="Senior Engineer" + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + await client.update_user_async(input=user_input, id="user-123") + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "PATCH" + assert "/v1.0/users/user-123" in call_args[0][1] + + @pytest.mark.asyncio + async def test_with_all_fields(self, mock_token_provider): + """Test update with all optional fields.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=204, text="") + user_input = UpdateUserRequest( + user_principal_name="john.doe@contoso.com", + display_name="John Doe", + mail_nickname="johndoe", + given_name="John", + surname="Doe", + account_enabled=True, + job_title="Senior Engineer", + department="Engineering", + mobile_phone="+1-555-123-4567", + office_location="Building A", + preferred_language="en-US", + business_phones=["+1-555-987-6543"], + other_mails=["john.personal@email.com"] + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + await client.update_user_async(input=user_input, id="user-123") + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid user data"}') + user_input = UpdateUserRequest(display_name="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.update_user_async(input=user_input, id="user-123") + + assert exc_info.value.status_code == 400 + + +class TestRefreshTokens: + """Tests for refresh_tokens_async method (void operation).""" + + @pytest.mark.asyncio + async def test_success(self, mock_token_provider): + """Test successful POST request to revoke sign-in sessions.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + await client.refresh_tokens_async(id="user-123") + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "POST" + assert "/v1.0/users/user-123/revokeSignInSessions" in call_args[0][1] + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "User not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.refresh_tokens_async(id="nonexistent-user") + + assert exc_info.value.status_code == 404 + + +class TestGetGroupMembers: + """Tests for get_group_members_async method.""" + + @pytest.mark.asyncio + async def test_success_with_members(self, mock_token_provider): + """Test successful GET request returns group members.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text=( + '{"value": [{"id": "user1", "displayName": "User One"}, ' + '{"id": "user2", "displayName": "User Two"}], "nextLink": null}' + ) + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.get_group_members_async(id="group-123") + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "GET" + assert "/v1.0/groups/group-123/members" in call_args[0][1] + assert len(result["value"]) == 2 + + @pytest.mark.asyncio + async def test_with_top_parameter(self, mock_token_provider): + """Test GET request with top query parameter.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"value": [{"id": "user1"}], "nextLink": "https://graph.microsoft.com/next"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.get_group_members_async(id="group-123", top="10") + + call_args = mock_send.call_args + assert "$top=10" in call_args[0][1] + assert result["nextLink"] is not None + + @pytest.mark.asyncio + async def test_empty_response_returns_none(self, mock_token_provider): + """Test that empty response returns None.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_group_members_async(id="group-123") + assert result is None + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Group not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_group_members_async(id="nonexistent-group") + + assert exc_info.value.status_code == 404 + + +class TestAddUserToGroup: + """Tests for add_user_to_group_async method (void operation).""" + + @pytest.mark.asyncio + async def test_success(self, mock_token_provider): + """Test successful POST request to add user to group.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=204, text="") + user_ref = GetGroupRequest(id="user-456") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + await client.add_user_to_group_async(input=user_ref, id="group-123") + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "POST" + assert "/v1.0/groups/group-123/members/$ref" in call_args[0][1] + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "User already in group"}') + user_ref = GetGroupRequest(id="user-456") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.add_user_to_group_async(input=user_ref, id="group-123") + + assert exc_info.value.status_code == 400 + + +class TestAssignManager: + """Tests for assign_manager_async method (void operation).""" + + @pytest.mark.asyncio + async def test_success(self, mock_token_provider): + """Test successful PUT request to assign manager.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=204, text="") + manager_ref = AssignManagerRequest(id="manager-789") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + await client.assign_manager_async(input=manager_ref, id="user-123") + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "PUT" + assert "/v1.0/users/user-123/manager/$ref" in call_args[0][1] + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "User not found"}') + manager_ref = AssignManagerRequest(id="manager-789") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.assign_manager_async(input=manager_ref, id="nonexistent-user") + + assert exc_info.value.status_code == 404 + + +class TestCheckMemberGroups: + """Tests for check_member_groups_async method.""" + + @pytest.mark.asyncio + async def test_success_user_is_member(self, mock_token_provider): + """Test successful check when user is member of group.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"value": ["group-123"]}' + ) + check_input = CheckMemberGroupsRequest(group_ids=["group-123", "group-456"]) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.check_member_groups_async(input=check_input, id="user-123") + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "POST" + assert "/v2/v1.0/users/user-123/checkMemberGroups" in call_args[0][1] + assert "group-123" in result["value"] + + @pytest.mark.asyncio + async def test_success_user_is_not_member(self, mock_token_provider): + """Test successful check when user is not member of any group.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text='{"value": []}') + check_input = CheckMemberGroupsRequest(group_ids=["group-123"]) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.check_member_groups_async(input=check_input, id="user-123") + assert result["value"] == [] + + @pytest.mark.asyncio + async def test_empty_response_returns_none(self, mock_token_provider): + """Test that empty response returns None.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text="") + check_input = CheckMemberGroupsRequest(group_ids=["group-123"]) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.check_member_groups_async(input=check_input, id="user-123") + assert result is None + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "User not found"}') + check_input = CheckMemberGroupsRequest(group_ids=["group-123"]) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.check_member_groups_async(input=check_input, id="nonexistent-user") + + assert exc_info.value.status_code == 404 + + +class TestGetMemberGroups: + """Tests for get_member_groups_async method.""" + + @pytest.mark.asyncio + async def test_success_with_groups(self, mock_token_provider): + """Test successful POST request returns user's groups.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"value": ["group-1", "group-2", "group-3"]}' + ) + member_input = GetMemberGroupsRequest(security_enabled_only=False) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.get_member_groups_async(input=member_input, id="user-123") + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "POST" + assert "/v2/v1.0/users/user-123/getMemberGroups" in call_args[0][1] + assert len(result["value"]) == 3 + + @pytest.mark.asyncio + async def test_with_security_enabled_only(self, mock_token_provider): + """Test with security_enabled_only filter.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text='{"value": ["sec-group-1"]}') + member_input = GetMemberGroupsRequest(security_enabled_only=True) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_member_groups_async(input=member_input, id="user-123") + assert len(result["value"]) == 1 + + @pytest.mark.asyncio + async def test_empty_response_returns_none(self, mock_token_provider): + """Test that empty response returns None.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text="") + member_input = GetMemberGroupsRequest() + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_member_groups_async(input=member_input, id="user-123") + assert result is None + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzureadClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=403, text='{"error": "Insufficient permissions"}') + member_input = GetMemberGroupsRequest() + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_member_groups_async(input=member_input, id="user-123") + + assert exc_info.value.status_code == 403 + + +class TestAdditionalDataClasses: + """Tests for additional data class creation and attributes.""" + + def test_update_user_request_creation(self): + """Test UpdateUserRequest dataclass creation.""" + request = UpdateUserRequest( + user_principal_name="john@contoso.com", + display_name="John Doe", + job_title="Engineer", + department="IT" + ) + + assert request.user_principal_name == "john@contoso.com" + assert request.display_name == "John Doe" + assert request.job_title == "Engineer" + assert request.department == "IT" + + def test_get_group_request_creation(self): + """Test GetGroupRequest dataclass creation.""" + request = GetGroupRequest(id="5e6cf5c7-b511-4842-6aae-3f6b8ae5e95b") + + assert request.id == "5e6cf5c7-b511-4842-6aae-3f6b8ae5e95b" + + def test_assign_manager_request_creation(self): + """Test AssignManagerRequest dataclass creation.""" + request = AssignManagerRequest(id="manager-id-123") + + assert request.id == "manager-id-123" + + def test_check_member_groups_request_creation(self): + """Test CheckMemberGroupsRequest dataclass creation.""" + request = CheckMemberGroupsRequest( + group_ids=["group-1", "group-2", "group-3"] + ) + + assert len(request.group_ids) == 3 + assert "group-1" in request.group_ids + + def test_get_member_groups_request_creation(self): + """Test GetMemberGroupsRequest dataclass creation.""" + request = GetMemberGroupsRequest(security_enabled_only=True) + + assert request.security_enabled_only is True diff --git a/tests/test_azurequeues.py b/tests/test_azurequeues.py index cc1b129..bc2836e 100644 --- a/tests/test_azurequeues.py +++ b/tests/test_azurequeues.py @@ -170,6 +170,32 @@ async def test_without_popreceipt(self, mock_token_provider): url = call_args[0][1] assert "popreceipt=" not in url + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzurequeuesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Message not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.delete_message_async( + storage_account_name="mystorageaccount", + queue_name="myqueue", + message_id="nonexistent", + popreceipt="receipt-abc" + ) + + assert exc_info.value.status_code == 404 + class TestGetMessages: """Tests for get_messages_async method.""" @@ -310,6 +336,29 @@ async def test_empty_response_returns_none(self, mock_token_provider): ) assert result is None + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzurequeuesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Storage account not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.list_queues_async( + storage_account_name="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestOnMessages: """Tests for on_messages_async method (trigger).""" @@ -370,6 +419,30 @@ async def test_with_visibilitytimeout(self, mock_token_provider): url = call_args[0][1] assert "visibilitytimeout=60" in url + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzurequeuesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Queue not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.on_messages_async( + storage_account_name="mystorageaccount", + queue_name="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestOnMessageThresholdReached: """Tests for on_message_threshold_reached_async method (trigger).""" @@ -469,6 +542,32 @@ async def test_success(self, mock_token_provider): body = call_args.kwargs.get('body') or call_args[1].get('body') assert body is message_input + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzurequeuesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid message format"}') + message_input = PutMessageInput(additional_properties={}) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.put_message_async( + input=message_input, + storage_account_name="mystorageaccount", + queue_name="myqueue" + ) + + assert exc_info.value.status_code == 400 + class TestPutQueue: """Tests for put_queue_async method.""" diff --git a/tests/test_azuretables.py b/tests/test_azuretables.py index 0fc5c8e..e9ccea0 100644 --- a/tests/test_azuretables.py +++ b/tests/test_azuretables.py @@ -214,6 +214,31 @@ async def test_success_with_json_response(self, mock_token_provider): assert "/storageAccounts/mystorageaccount/tables" in call_args[0][1] assert result["TableName"] == "newtable" + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzuretablesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=409, text='{"error": "Table already exists"}') + table_input = CreateTableInput(additional_properties={"TableName": "existingtable"}) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.create_table_async( + input=table_input, + storage_account_name="mystorageaccount" + ) + + assert exc_info.value.status_code == 409 + class TestDeleteEntity: """Tests for delete_entity_async method.""" @@ -246,6 +271,32 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "DELETE" assert "/etag(PartitionKey='pk1',RowKey='rk1')" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzuretablesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Entity not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.delete_entity_async( + storage_account_name="mystorageaccount", + table_name="mytable", + partition_key="nonexistent", + row_key="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestDeleteTable: """Tests for delete_table_async method.""" @@ -276,6 +327,30 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "DELETE" assert "/storageAccounts/mystorageaccount/tables/mytable" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzuretablesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Table not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.delete_table_async( + storage_account_name="mystorageaccount", + table_name="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestGetEntities: """Tests for get_entities_async method.""" @@ -433,6 +508,32 @@ async def test_with_select_parameter(self, mock_token_provider): url = call_args[0][1] assert "$select=Name" in url + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzuretablesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Entity not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_entity_async( + storage_account_name="mystorageaccount", + table_name="mytable", + partition_key="nonexistent", + row_key="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestGetTable: """Tests for get_table_async method.""" @@ -467,6 +568,30 @@ async def test_success_with_json_response(self, mock_token_provider): assert "/storageAccounts/mystorageaccount/tables/mytable" in call_args[0][1] assert result["TableName"] == "mytable" + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzuretablesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Table not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_table_async( + storage_account_name="mystorageaccount", + table_name="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestGetTables: """Tests for get_tables_async method.""" @@ -521,6 +646,29 @@ async def test_empty_response_returns_none(self, mock_token_provider): ) assert result is None + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzuretablesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Storage account not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_tables_async( + storage_account_name="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestInsertMergeEntity: """Tests for insert_merge_entity_async method.""" @@ -557,6 +705,34 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "PATCH" assert "/entities(PartitionKey='pk1',RowKey='rk1')" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzuretablesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid entity data"}') + entity_input = InsertMergeEntityInput(additional_properties={}) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.insert_merge_entity_async( + input=entity_input, + storage_account_name="mystorageaccount", + table_name="mytable", + partition_key="pk1", + row_key="rk1" + ) + + assert exc_info.value.status_code == 400 + class TestInsertReplaceEntity: """Tests for insert_replace_entity_async method.""" @@ -593,6 +769,34 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "PUT" assert "/entities(PartitionKey='pk1',RowKey='rk1')" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzuretablesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid entity data"}') + entity_input = InsertReplaceEntityInput(additional_properties={}) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.insert_replace_entity_async( + input=entity_input, + storage_account_name="mystorageaccount", + table_name="mytable", + partition_key="pk1", + row_key="rk1" + ) + + assert exc_info.value.status_code == 400 + class TestMergeEntity: """Tests for merge_entity_async method.""" @@ -629,6 +833,34 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "PATCH" assert "/etag(PartitionKey='pk1',RowKey='rk1')" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzuretablesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Entity not found"}') + entity_input = MergeEntityInput(additional_properties={}) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.merge_entity_async( + input=entity_input, + storage_account_name="mystorageaccount", + table_name="mytable", + partition_key="nonexistent", + row_key="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestReplaceEntity: """Tests for replace_entity_async method.""" @@ -665,6 +897,34 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "PUT" assert "/etag(PartitionKey='pk1',RowKey='rk1')" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = AzuretablesClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Entity not found"}') + entity_input = ReplaceEntityInput(additional_properties={}) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.replace_entity_async( + input=entity_input, + storage_account_name="mystorageaccount", + table_name="mytable", + partition_key="nonexistent", + row_key="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestDataClasses: """Tests for data class creation and attributes.""" diff --git a/tests/test_code_quality.py b/tests/test_code_quality.py index ed20f50..a980bb0 100644 --- a/tests/test_code_quality.py +++ b/tests/test_code_quality.py @@ -1,15 +1,25 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import ast import pathlib import subprocess import sys import unittest +from typing import NamedTuple ROOT_PATH = pathlib.Path(__file__).parent.parent +class LintViolation(NamedTuple): + """A lint violation in generated code.""" + + file: str + line: int + message: str + + class TestCodeQuality(unittest.TestCase): def test_mypy(self): try: @@ -53,3 +63,88 @@ def test_flake8(self): output = ex.output.decode() raise AssertionError( f'flake8 validation failed:\n{output}') from None + + def test_no_unchecked_send_async_calls(self): + """Verify all send_async calls capture response and check status. + + Generated connector code must not discard send_async responses. + Void operations (DELETE, PATCH, fire-and-forget POST) must capture + the response and raise ConnectorException on non-2xx status codes. + + See GENERATION.md "Generated Client Acceptance Criteria" for details. + """ + connector_dir = ROOT_PATH / 'src' / 'azure' / 'connectors' + + # Skip files that are not generated connectors + skip_files = {'__init__.py', 'sdk.py'} + + violations: list[LintViolation] = [] + + for filepath in connector_dir.glob('*.py'): + if filepath.name in skip_files: + continue + + violations.extend(self._find_unchecked_send_async(filepath)) + + if violations: + # Format violations for readable output + messages = [ + f" {v.file}:{v.line}: {v.message}" + for v in sorted(violations, key=lambda x: (x.file, x.line)) + ] + violation_text = "\n".join(messages) + + raise AssertionError( + f"Found {len(violations)} unchecked send_async call(s).\n" + f"Void operations must capture response and raise " + f"ConnectorException on non-2xx.\n" + f"Fix the generator (see GENERATION.md) and regenerate, " + f"or regenerate with the latest generator.\n\n" + f"{violation_text}" + ) + + def _find_unchecked_send_async( + self, filepath: pathlib.Path + ) -> list[LintViolation]: + """Find send_async calls whose result is not captured. + + The correct pattern is: + response = await self.http_client.send_async(...) + if not (200 <= response.status < 300): + raise ConnectorException(...) + + The incorrect pattern is: + await self.http_client.send_async(...) # Result discarded! + """ + violations: list[LintViolation] = [] + + try: + source = filepath.read_text(encoding='utf-8') + tree = ast.parse(source) + except SyntaxError: + # If file has syntax errors, skip (mypy/flake8 will catch it) + return violations + + for node in ast.walk(tree): + # Look for Expr nodes containing Await + # (Expr means the value is discarded, not assigned) + if isinstance(node, ast.Expr) and isinstance(node.value, ast.Await): + await_expr = node.value.value + + # Check if this is a call to send_async + if isinstance(await_expr, ast.Call): + func = await_expr.func + + # Match: self.http_client.send_async(...) or + # self._http_client.send_async(...) + if isinstance(func, ast.Attribute) and func.attr == 'send_async': + if isinstance(func.value, ast.Attribute): + attr_name = func.value.attr + if attr_name in ('http_client', '_http_client'): + violations.append(LintViolation( + file=filepath.name, + line=node.lineno, + message='Unchecked send_async call' + )) + + return violations diff --git a/tests/test_commondataservice.py b/tests/test_commondataservice.py index 66265b5..330af7c 100644 --- a/tests/test_commondataservice.py +++ b/tests/test_commondataservice.py @@ -387,6 +387,29 @@ async def test_delete_record_with_partition(self, mock_token_provider): url = mock_send.call_args[0][1] assert "partitionId=" in url + @pytest.mark.asyncio + async def test_delete_record_error(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = CommondataserviceClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text="Record not found") + + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock + ) as mock_send: + mock_send.return_value = mock_response + + with pytest.raises(ConnectorException) as exc_info: + await client.delete_record_async( + entity_name="accounts", + record_id="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestUpdateRecord: """Tests for update_record_async method.""" @@ -481,6 +504,33 @@ async def test_upload_file_success(self, mock_token_provider): assert "/annotations(123)/documentbody" in call_args[0][1] assert "x-ms-file-name=" in call_args[0][1] + @pytest.mark.asyncio + async def test_upload_file_error(self, mock_token_provider): + """Test that upload error raises ConnectorException.""" + client = CommondataserviceClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=413, text="File too large") + + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock + ) as mock_send: + mock_send.return_value = mock_response + + input_data = UpdateEntityFileImageFieldContentInput() + with pytest.raises(ConnectorException) as exc_info: + await client.update_entity_file_image_field_content_async( + input=input_data, + entity_name="annotations", + record_id="123", + file_image_field_name="documentbody", + x_ms_file_name="large-file.pdf" + ) + + assert exc_info.value.status_code == 413 + @pytest.mark.asyncio async def test_download_file_success(self, mock_token_provider): """Test successful file download.""" @@ -707,6 +757,34 @@ async def test_associate_entities_success(self, mock_token_provider): assert call_args[0][0] == "POST" assert "/accounts(123)/contact_customer_accounts/$ref" in call_args[0][1] + @pytest.mark.asyncio + async def test_associate_entities_error(self, mock_token_provider): + """Test that association error raises ConnectorException.""" + client = CommondataserviceClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text="Invalid relationship") + + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock + ) as mock_send: + mock_send.return_value = mock_response + + input_data = AssociateEntityRequest( + id="https://org.crm.dynamics.com/api/data/v9.0/contacts(456)" + ) + with pytest.raises(ConnectorException) as exc_info: + await client.associate_entities_async( + input=input_data, + entity_name="accounts", + record_id="123", + association_entity_relationship="invalid_relationship" + ) + + assert exc_info.value.status_code == 400 + @pytest.mark.asyncio async def test_disassociate_entities_success(self, mock_token_provider): """Test successful entity disassociation.""" @@ -734,6 +812,31 @@ async def test_disassociate_entities_success(self, mock_token_provider): assert "$ref" in call_args[0][1] assert "$id=" in call_args[0][1] + @pytest.mark.asyncio + async def test_disassociate_entities_error(self, mock_token_provider): + """Test that disassociation error raises ConnectorException.""" + client = CommondataserviceClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text="Relationship not found") + + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock + ) as mock_send: + mock_send.return_value = mock_response + + with pytest.raises(ConnectorException) as exc_info: + await client.disassociate_entities_async( + entity_name="accounts", + record_id="123", + association_entity_relationship="nonexistent", + id="https://org.crm.dynamics.com/api/data/v9.0/contacts(456)" + ) + + assert exc_info.value.status_code == 404 + class TestSearchRecords: """Tests for search operations.""" @@ -820,6 +923,32 @@ async def test_subscribe_webhook_trigger(self, mock_token_provider): assert call_args[0][0] == "POST" assert "/callbackregistrations" in call_args[0][1] + @pytest.mark.asyncio + async def test_subscribe_webhook_trigger_error(self, mock_token_provider): + """Test webhook subscription error.""" + client = CommondataserviceClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text="Invalid callback registration") + + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock + ) as mock_send: + mock_send.return_value = mock_response + + input_data = CallbackRegistration( + url="invalid-url", + entityname="accounts", + message=1, + scope=1 + ) + with pytest.raises(ConnectorException) as exc_info: + await client.subscribe_webhook_trigger_async(input=input_data) + + assert exc_info.value.status_code == 400 + @pytest.mark.asyncio async def test_business_events_trigger(self, mock_token_provider): """Test business events trigger subscription.""" @@ -847,6 +976,30 @@ async def test_business_events_trigger(self, mock_token_provider): assert call_args[0][0] == "POST" assert "/v9.2/callbackregistrations" in call_args[0][1] + @pytest.mark.asyncio + async def test_business_events_trigger_error(self, mock_token_provider): + """Test business events trigger error.""" + client = CommondataserviceClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text="Invalid trigger configuration") + + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock + ) as mock_send: + mock_send.return_value = mock_response + + input_data = WhenAnActionIsPerformedSubscriptionRequest( + url="invalid-url", + entityname="accounts" + ) + with pytest.raises(ConnectorException) as exc_info: + await client.business_events_trigger_async(input=input_data) + + assert exc_info.value.status_code == 400 + class TestExecuteChangeset: """Tests for changeset operations.""" @@ -872,6 +1025,26 @@ async def test_execute_changeset_success(self, mock_token_provider): assert call_args[0][0] == "POST" assert "/$batch" in call_args[0][1] + @pytest.mark.asyncio + async def test_execute_changeset_error(self, mock_token_provider): + """Test changeset execution error.""" + client = CommondataserviceClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text="Changeset validation failed") + + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock + ) as mock_send: + mock_send.return_value = mock_response + + with pytest.raises(ConnectorException) as exc_info: + await client.execute_changeset_async() + + assert exc_info.value.status_code == 400 + class TestDataclasses: """Tests for dataclass serialization.""" diff --git a/tests/test_eventhubs.py b/tests/test_eventhubs.py index e8e9af8..93fb41e 100644 --- a/tests/test_eventhubs.py +++ b/tests/test_eventhubs.py @@ -333,6 +333,31 @@ async def test_with_partition_key(self, mock_token_provider): url = call_args[0][1] assert "partitionKey=partition-1" in url + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = EventhubsClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid event format"}') + event_input = SendEvent(content_data='{"invalid": true}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.send_event_async( + input=event_input, + event_hub_name="myeventhub" + ) + + assert exc_info.value.status_code == 400 + class TestSendEvents: """Tests for send_events_async method.""" @@ -399,6 +424,31 @@ async def test_with_partition_key(self, mock_token_provider): assert "partitionKey=partition-2" in url assert "/events/batch" in url + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = EventhubsClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=413, text='{"error": "Batch too large"}') + events_input = SendEventsInput(additional_properties={"events": []}) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.send_events_async( + input=events_input, + event_hub_name="myeventhub" + ) + + assert exc_info.value.status_code == 413 + class TestDataClasses: """Tests for data class creation and attributes.""" diff --git a/tests/test_excelonlinebusiness.py b/tests/test_excelonlinebusiness.py index 05e6c3f..ab29ad3 100644 --- a/tests/test_excelonlinebusiness.py +++ b/tests/test_excelonlinebusiness.py @@ -215,6 +215,34 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "POST" assert "/createIdColumn" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid table"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.create_id_column_async( + drive="drive-id", + file="file-id", + table="InvalidTable", + source="me", + id_column="ID", + populate_column="true" + ) + + assert exc_info.value.status_code == 400 + class TestGetItems: """Tests for get_items_async method.""" @@ -285,6 +313,32 @@ async def test_with_query_parameters(self, mock_token_provider): assert "$top=" in url assert "$skip=" in url + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Table not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_items_async( + drive="drive-id", + file="file-id", + table="NonexistentTable", + source="me" + ) + + assert exc_info.value.status_code == 404 + class TestGetComments: """Tests for get_comments_async method.""" @@ -319,6 +373,30 @@ async def test_success_with_json_response(self, mock_token_provider): assert "/workbook/comments" in call_args[0][1] assert result["value"][0]["id"] == "comment1" + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "File not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_comments_async( + drive="drive-id", + file="nonexistent-file" + ) + + assert exc_info.value.status_code == 404 + class TestGetComment: """Tests for get_comment_async method.""" @@ -354,6 +432,31 @@ async def test_success_with_json_response(self, mock_token_provider): assert "/comments/comment1" in call_args[0][1] assert result["content"] == "Please review" + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Comment not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_comment_async( + drive="drive-id", + file="file-id", + commentid="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestReplyComment: """Tests for reply_comment_async method.""" @@ -394,6 +497,33 @@ async def test_success_with_json_response(self, mock_token_provider): assert "/replies" in call_args[0][1] assert result["content"] == "I will review it" + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid comment reply"}') + comment_details = CommentDetails(content="", content_type="plain") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.reply_comment_async( + input=comment_details, + drive="drive-id", + file="file-id", + commentid="comment1" + ) + + assert exc_info.value.status_code == 400 + class TestGetItem: """Tests for get_item_async method.""" @@ -432,6 +562,34 @@ async def test_success_with_json_response(self, mock_token_provider): assert "/items/row-123" in call_args[0][1] assert result["Name"] == "John Doe" + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Row not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_item_async( + drive="drive-id", + file="file-id", + table="Table1", + id="nonexistent", + source="me", + id_column="ID" + ) + + assert exc_info.value.status_code == 404 + class TestDeleteItem: """Tests for delete_item_async method.""" @@ -466,6 +624,34 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "DELETE" assert "/items/row-123" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Row not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.delete_item_async( + drive="drive-id", + file="file-id", + table="Table1", + id="nonexistent", + source="me", + id_column="ID" + ) + + assert exc_info.value.status_code == 404 + class TestPatchItem: """Tests for patch_item_async method.""" @@ -578,6 +764,286 @@ async def test_success_with_json_response(self, mock_token_provider): assert "/officescripting/api/unattended/run" in call_args[0][1] assert result["result"] == "Script executed successfully" + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Script execution failed"}') + script_input = RunScriptProdInput(additional_properties={}) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.run_script_prod_async( + input=script_input, + drive="drive-id", + file="file-id", + script_drive="script-drive-id", + script_id="invalid-script", + source="me", + script_source="sites" + ) + + assert exc_info.value.status_code == 400 + + +class TestGetAllWorksheets: + """Tests for get_all_worksheets_async method.""" + + @pytest.mark.asyncio + async def test_success_with_json_response(self, mock_token_provider): + """Test successful GET request.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"value": [{"id": "sheet1", "name": "Sheet1", "position": 0}]}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.get_all_worksheets_async( + drive="drive-id", + file="file-id", + source="me" + ) + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "GET" + assert "/worksheets" in call_args[0][1] + assert result["value"][0]["name"] == "Sheet1" + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "File not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_all_worksheets_async( + drive="drive-id", + file="nonexistent", + source="me" + ) + + assert exc_info.value.status_code == 404 + + +class TestCreateWorksheet: + """Tests for create_worksheet_async method.""" + + @pytest.mark.asyncio + async def test_success_with_json_response(self, mock_token_provider): + """Test successful POST request.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=201, + text='{"id": "newsheet", "name": "NewSheet", "position": 1}' + ) + worksheet_input = CreateWorksheetInput(name="NewSheet") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.create_worksheet_async( + input=worksheet_input, + drive="drive-id", + file="file-id", + source="me" + ) + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "POST" + assert "/worksheets" in call_args[0][1] + assert result["name"] == "NewSheet" + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid worksheet name"}') + worksheet_input = CreateWorksheetInput(name="") + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.create_worksheet_async( + input=worksheet_input, + drive="drive-id", + file="file-id", + source="me" + ) + + assert exc_info.value.status_code == 400 + + +class TestGetTables: + """Tests for get_tables_async method.""" + + @pytest.mark.asyncio + async def test_success_with_json_response(self, mock_token_provider): + """Test successful GET request.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"value": [{"name": "Table1"}, {"name": "Table2"}]}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.get_tables_async( + drive="drive-id", + file="file-id", + source="me" + ) + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "GET" + assert "/tables" in call_args[0][1] + assert len(result["value"]) == 2 + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "File not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_tables_async( + drive="drive-id", + file="nonexistent", + source="me" + ) + + assert exc_info.value.status_code == 404 + + +class TestAddRow: + """Tests for add_row_async method.""" + + @pytest.mark.asyncio + async def test_success_with_json_response(self, mock_token_provider): + """Test successful POST request.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=201, + text='{"Name": "New Item", "Value": 250}' + ) + item_input = Item(dynamic_properties={"Name": "New Item", "Value": 250}) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + result = await client.add_row_async( + input=item_input, + drive="drive-id", + file="file-id", + table="Table1", + source="me" + ) + + mock_send.assert_called_once() + call_args = mock_send.call_args + assert call_args[0][0] == "POST" + assert "/rows" in call_args[0][1] + assert result["Name"] == "New Item" + + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ExcelonlinebusinessClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid row data"}') + item_input = Item(dynamic_properties={}) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.add_row_async( + input=item_input, + drive="drive-id", + file="file-id", + table="Table1", + source="me" + ) + + assert exc_info.value.status_code == 400 + class TestDataClasses: """Tests for data class creation and attributes.""" diff --git a/tests/test_office365.py b/tests/test_office365.py index f8417b4..2626023 100644 --- a/tests/test_office365.py +++ b/tests/test_office365.py @@ -309,6 +309,28 @@ async def test_success_no_return_value(self, mock_token_provider): assert call_args[1]["body"] is input_message assert result is None + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = Office365Client( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Draft not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + input_message = ClientDraftHtmlMessage() + await client.update_draft_email_async(input_message, "nonexistent") + + assert exc_info.value.status_code == 404 + class TestSendDraftEmail: """Tests for send_draft_email_async method.""" @@ -359,6 +381,27 @@ async def test_path_parameter_construction(self, mock_token_provider): path = call_args[0][1] assert "https://example.azure.com/connections/test/Draft/Send/" in path + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = Office365Client( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Draft not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.send_draft_email_async("nonexistent") + + assert exc_info.value.status_code == 404 + class TestAssignCategory: """Tests for assign_category_async method.""" @@ -389,6 +432,30 @@ async def test_success_with_multiple_query_params(self, mock_token_provider): assert "messageId=msg123" in path assert "category=Red%20category" in path + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = Office365Client( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Message not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.assign_category_async( + message_id="nonexistent", + category="Red category" + ) + + assert exc_info.value.status_code == 404 + class TestDeleteEmail: """Tests for delete_email_async method.""" diff --git a/tests/test_onedrive.py b/tests/test_onedrive.py index e3b2881..aa97c20 100644 --- a/tests/test_onedrive.py +++ b/tests/test_onedrive.py @@ -235,6 +235,27 @@ async def test_with_infer_content_type_parameter(self, mock_token_provider): call_args = mock_send.call_args assert "inferContentType=true" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OnedriveClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "File not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_file_content_async(id="nonexistent") + + assert exc_info.value.status_code == 404 + class TestGetFileContentByPath: """Tests for get_file_content_by_path_async method.""" @@ -267,6 +288,27 @@ async def test_success_returns_binary_content(self, mock_token_provider): assert "GetFileContentByPath" in call_args[0][1] assert result == binary_content + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OnedriveClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Path not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_file_content_by_path_async(path="/invalid/path") + + assert exc_info.value.status_code == 404 + class TestCreateFile: """Tests for create_file_async method.""" @@ -304,6 +346,31 @@ async def test_success_with_json_response(self, mock_token_provider): assert "name=" in call_args[0][1] assert result["id"] == "newfile123" + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OnedriveClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid folder path"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.create_file_async( + input=CreateFileInput(), + folder_path="/invalid", + name="file.txt" + ) + + assert exc_info.value.status_code == 400 + class TestUpdateFile: """Tests for update_file_async method.""" @@ -339,6 +406,30 @@ async def test_success_with_json_response(self, mock_token_provider): assert "/files/file123" in call_args[0][1] assert result["name"] == "updated.txt" + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OnedriveClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "File not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.update_file_async( + input=UpdateFileInput(), + id="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestDeleteFile: """Tests for delete_file_async method.""" @@ -366,6 +457,27 @@ async def test_success_returns_none(self, mock_token_provider): assert call_args[0][0] == "DELETE" assert "/files/file123" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OnedriveClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "File not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.delete_file_async(id="nonexistent") + + assert exc_info.value.status_code == 404 + class TestCopyFile: """Tests for copy_file_async method.""" @@ -427,6 +539,30 @@ async def test_with_overwrite_parameter(self, mock_token_provider): call_args = mock_send.call_args assert "overwrite=true" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OnedriveClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid destination"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.copy_file_async( + source="invalid", + destination="invalid" + ) + + assert exc_info.value.status_code == 400 + class TestCopyDriveFile: """Tests for copy_drive_file_async method.""" @@ -462,6 +598,30 @@ async def test_success_with_json_response(self, mock_token_provider): assert "destination=" in call_args[0][1] assert result["id"] == "copiedfile123" + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OnedriveClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Source file not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.copy_drive_file_async( + id="nonexistent", + destination="/Documents/copy.txt" + ) + + assert exc_info.value.status_code == 404 + class TestMoveFile: """Tests for move_file_async method.""" @@ -497,6 +657,30 @@ async def test_success_with_json_response(self, mock_token_provider): assert "destination=" in call_args[0][1] assert result["path"] == "/Archive/moved_file.txt" + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OnedriveClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "File not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.move_file_async( + id="nonexistent", + destination="/Archive/file.txt" + ) + + assert exc_info.value.status_code == 404 + class TestListFolder: """Tests for list_folder_async method.""" diff --git a/tests/test_outlook.py b/tests/test_outlook.py index a0c5f43..b86e957 100644 --- a/tests/test_outlook.py +++ b/tests/test_outlook.py @@ -212,6 +212,27 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "DELETE" assert "/Mail/msg123" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OutlookClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Message not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.delete_email_async(message_id="nonexistent") + + assert exc_info.value.status_code == 404 + class TestMoveEmail: """Tests for move_async method.""" @@ -243,6 +264,27 @@ async def test_success_with_json_response(self, mock_token_provider): assert "/Mail/Move/msg123" in call_args[0][1] assert "folderPath=Archive" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OutlookClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Message not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.move_async(message_id="nonexistent", folder_path="Archive") + + assert exc_info.value.status_code == 404 + class TestFlagEmail: """Tests for flag_async method.""" @@ -270,6 +312,27 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "POST" assert "/Mail/Flag/msg123" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OutlookClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Message not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.flag_async(message_id="nonexistent") + + assert exc_info.value.status_code == 404 + class TestMarkAsRead: """Tests for mark_as_read_async method.""" @@ -297,6 +360,27 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "POST" assert "/Mail/MarkAsRead/msg123" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OutlookClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Message not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.mark_as_read_async(message_id="nonexistent") + + assert exc_info.value.status_code == 404 + class TestGetAttachment: """Tests for get_attachment_async method.""" @@ -328,6 +412,30 @@ async def test_success(self, mock_token_provider): assert "/Mail/msg123/Attachments/att456" in call_args[0][1] assert isinstance(result, bytes) + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OutlookClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Attachment not found"}') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_attachment_async( + message_id="msg123", + attachment_id="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestSendEmail: """Tests for send_email_async method.""" @@ -360,6 +468,32 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "POST" assert "/v2/Mail" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OutlookClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid email format"}') + email_input = ClientSendHtmlMessage( + to="invalid", + subject="Test", + body="Test" + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.send_email_async(input=email_input) + + assert exc_info.value.status_code == 400 + class TestReplyTo: """Tests for reply_to_async method.""" @@ -391,6 +525,31 @@ async def test_success(self, mock_token_provider): assert call_args[0][0] == "POST" assert "/v3/Mail/ReplyTo/msg123" in call_args[0][1] + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = OutlookClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=404, text='{"error": "Message not found"}') + reply_input = ReplyHtmlMessage( + body="
Reply
", + reply_all=False + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.reply_to_async(input=reply_input, message_id="nonexistent") + + assert exc_info.value.status_code == 404 + class TestGetEmails: """Tests for get_emails_async method.""" diff --git a/tests/test_servicebus.py b/tests/test_servicebus.py index e4402d3..2c13ea8 100644 --- a/tests/test_servicebus.py +++ b/tests/test_servicebus.py @@ -175,6 +175,24 @@ async def test_send_messages_batch_success(self, mock_token_provider): assert call_args[0][0] == "POST" assert "myqueue/messages/batch" in call_args[0][1] + @pytest.mark.asyncio + async def test_send_messages_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(400, '{"error": "Invalid batch format"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + input_data = SendMessagesInput() + await client.send_messages_async(input=input_data, entity_name="myqueue") + + assert exc_info.value.status_code == 400 + class TestGetMessageFromQueue: """Tests for get_message_from_queue_async method.""" @@ -299,6 +317,25 @@ async def test_get_message_peek_lock_with_session(self, mock_token_provider): call_args = mock_send.call_args assert "sessionId=session-abc" in call_args[0][1] + @pytest.mark.asyncio + async def test_peek_lock_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Queue not found"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_new_message_from_queue_with_peek_lock_async( + queue_name="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestCompleteMessageInQueue: """Tests for complete_message_in_queue_async method.""" @@ -326,6 +363,26 @@ async def test_complete_message_success(self, mock_token_provider): assert "myqueue/messages/complete" in call_args[0][1] assert "lockToken=token-123" in call_args[0][1] + @pytest.mark.asyncio + async def test_complete_message_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Lock token expired"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.complete_message_in_queue_async( + queue_name="myqueue", + lock_token="expired-token" + ) + + assert exc_info.value.status_code == 404 + class TestAbandonMessageInQueue: """Tests for abandon_message_in_queue_async method.""" @@ -352,6 +409,26 @@ async def test_abandon_message_success(self, mock_token_provider): assert call_args[0][0] == "POST" assert "myqueue/messages/abandon" in call_args[0][1] + @pytest.mark.asyncio + async def test_abandon_message_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Lock token expired"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.abandon_message_in_queue_async( + queue_name="myqueue", + lock_token="expired-token" + ) + + assert exc_info.value.status_code == 404 + class TestDeferMessageInQueue: """Tests for defer_message_in_queue_async method.""" @@ -377,6 +454,26 @@ async def test_defer_message_success(self, mock_token_provider): call_args = mock_send.call_args assert "myqueue/messages/defer" in call_args[0][1] + @pytest.mark.asyncio + async def test_defer_message_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Lock token expired"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.defer_message_in_queue_async( + queue_name="myqueue", + lock_token="expired-token" + ) + + assert exc_info.value.status_code == 404 + class TestGetDeferredMessageFromQueue: """Tests for get_deferred_message_from_queue_async method.""" @@ -404,6 +501,26 @@ async def test_get_deferred_message_success(self, mock_token_provider): assert "sequenceNumber=42" in call_args[0][1] assert result["sequenceNumber"] == 42 + @pytest.mark.asyncio + async def test_get_deferred_message_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Message not found"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_deferred_message_from_queue_async( + queue_name="myqueue", + sequence_number="999" + ) + + assert exc_info.value.status_code == 404 + class TestDeadLetterMessageInQueue: """Tests for dead_letter_message_in_queue_async method.""" @@ -431,6 +548,27 @@ async def test_dead_letter_message_success(self, mock_token_provider): assert "myqueue/messages/deadletter" in call_args[0][1] assert "deadLetterReason=Processing%20failed" in call_args[0][1] + @pytest.mark.asyncio + async def test_dead_letter_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Lock token expired"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.dead_letter_message_in_queue_async( + queue_name="myqueue", + lock_token="expired-token", + dead_letter_reason="Failed" + ) + + assert exc_info.value.status_code == 404 + class TestRenewLockOnMessageInQueue: """Tests for renew_lock_on_message_in_queue_async method.""" @@ -499,6 +637,23 @@ async def test_get_messages_with_max_count(self, mock_token_provider): call_args = mock_send.call_args assert "maxMessageCount=10" in call_args[0][1] + @pytest.mark.asyncio + async def test_get_messages_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Queue not found"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_messages_from_queue_async(queue_name="nonexistent") + + assert exc_info.value.status_code == 404 + class TestCloseSessionInQueue: """Tests for close_session_in_queue_async method.""" @@ -525,6 +680,26 @@ async def test_close_session_success(self, mock_token_provider): assert call_args[0][0] == "DELETE" assert "myqueue/sessions/session-123/close" in call_args[0][1] + @pytest.mark.asyncio + async def test_close_session_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Session not found"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.close_session_in_queue_async( + queue_name="myqueue", + session_id="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestRenewLockOnSessionInQueue: """Tests for renew_lock_on_session_in_queue_async method.""" @@ -550,6 +725,26 @@ async def test_renew_session_lock_success(self, mock_token_provider): call_args = mock_send.call_args assert "myqueue/sessions/session-123/renewlock" in call_args[0][1] + @pytest.mark.asyncio + async def test_renew_session_lock_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Session not found"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.renew_lock_on_session_in_queue_async( + queue_name="myqueue", + session_id="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestGetMessageFromTopic: """Tests for get_message_from_topic_async method.""" @@ -576,6 +771,26 @@ async def test_get_topic_message_success(self, mock_token_provider): assert "mytopic/subscriptions/mysub/messages/head" in call_args[0][1] assert result["messageId"] == "msg-456" + @pytest.mark.asyncio + async def test_get_topic_message_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Subscription not found"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_message_from_topic_async( + topic_name="mytopic", + subscription_name="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestCompleteMessageInTopic: """Tests for complete_message_in_topic_async method.""" @@ -603,6 +818,27 @@ async def test_complete_topic_message_success(self, mock_token_provider): assert call_args[0][0] == "DELETE" assert "mytopic/subscriptions/mysub/messages/complete" in call_args[0][1] + @pytest.mark.asyncio + async def test_complete_topic_message_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Lock token expired"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.complete_message_in_topic_async( + topic_name="mytopic", + subscription_name="mysub", + lock_token="expired-token" + ) + + assert exc_info.value.status_code == 404 + class TestAbandonMessageInTopic: """Tests for abandon_message_in_topic_async method.""" @@ -629,6 +865,27 @@ async def test_abandon_topic_message_success(self, mock_token_provider): call_args = mock_send.call_args assert "mytopic/subscriptions/mysub/messages/abandon" in call_args[0][1] + @pytest.mark.asyncio + async def test_abandon_topic_message_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Lock token expired"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.abandon_message_in_topic_async( + topic_name="mytopic", + subscription_name="mysub", + lock_token="expired-token" + ) + + assert exc_info.value.status_code == 404 + class TestCreateTopicSubscription: """Tests for create_topic_subscription_async method.""" @@ -658,6 +915,28 @@ async def test_create_subscription_success(self, mock_token_provider): assert "mytopic/subscriptions/newsub" in call_args[0][1] assert result["subscriptionName"] == "newsub" + @pytest.mark.asyncio + async def test_create_subscription_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(400, '{"error": "Invalid subscription name"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + input_data = CreateTopicSubscriptionInput() + await client.create_topic_subscription_async( + input=input_data, + topic_name="mytopic", + subscription_name="invalid" + ) + + assert exc_info.value.status_code == 400 + class TestDeleteTopicSubscription: """Tests for delete_topic_subscription_async method.""" @@ -684,6 +963,26 @@ async def test_delete_subscription_success(self, mock_token_provider): assert call_args[0][0] == "DELETE" assert "mytopic/subscriptions/mysub" in call_args[0][1] + @pytest.mark.asyncio + async def test_delete_subscription_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = ServicebusClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(404, '{"error": "Subscription not found"}') + with patch.object( + client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.delete_topic_subscription_async( + topic_name="mytopic", + subscription_name="nonexistent" + ) + + assert exc_info.value.status_code == 404 + class TestGetMessagesFromTopic: """Tests for get_messages_from_topic_async method.""" diff --git a/tests/test_sharepointonline.py b/tests/test_sharepointonline.py index 2ae8dd0..52d6106 100644 --- a/tests/test_sharepointonline.py +++ b/tests/test_sharepointonline.py @@ -12,7 +12,14 @@ PostItemInput, PatchItemInput, CreateApprovalRequestInput, + CreateAttachmentInput, + FileCheckInParameters, ItemPermissionCreateLinkBody, + ItemGrantAccessBody, + MoveFileParameters, + CopyFolderParameters, + MoveFolderParameters, + PatchFileItemInput, ) from azure.connectors.sdk import ( ConnectorClientOptions, @@ -360,6 +367,89 @@ async def test_get_file_content(self, mock_token_provider): assert result == file_content + @pytest.mark.asyncio + async def test_create_file_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=400, + text='{"error": "Bad request"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.create_file_async( + CreateFileInput(), + "https://contoso.sharepoint.com/sites/site1", + "/Shared Documents", + "document.docx" + ) + + assert exc_info.value.status_code == 400 + + @pytest.mark.asyncio + async def test_get_file_metadata_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=404, + text='{"error": "File not found"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_file_metadata_async( + "https://contoso.sharepoint.com/sites/site1", + "missing_file" + ) + + assert exc_info.value.status_code == 404 + + @pytest.mark.asyncio + async def test_delete_file_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=403, + text='{"error": "Forbidden"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.delete_file_async( + "https://contoso.sharepoint.com/sites/site1", + "protected_file" + ) + + assert exc_info.value.status_code == 403 + class TestFolderOperations: """Tests for folder operation methods.""" @@ -418,6 +508,62 @@ async def test_get_folder_metadata(self, mock_token_provider): assert result["ItemCount"] == 10 + @pytest.mark.asyncio + async def test_create_folder_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=409, + text='{"error": "Folder already exists"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.create_new_folder_async( + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "/Shared Documents", + "ExistingFolder" + ) + + assert exc_info.value.status_code == 409 + + @pytest.mark.asyncio + async def test_get_folder_metadata_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=404, + text='{"error": "Folder not found"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_folder_metadata_async( + "https://contoso.sharepoint.com/sites/site1", + "missing_folder" + ) + + assert exc_info.value.status_code == 404 + class TestItemOperations: """Tests for list item operation methods.""" @@ -564,6 +710,89 @@ async def test_delete_item(self, mock_token_provider): assert call_args[0][0] == "DELETE" assert result is None + @pytest.mark.asyncio + async def test_get_items_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=404, + text='{"error": "List not found"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_items_async( + "https://contoso.sharepoint.com/sites/site1", + "MissingList" + ) + + assert exc_info.value.status_code == 404 + + @pytest.mark.asyncio + async def test_post_item_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=400, + text='{"error": "Invalid item data"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.post_item_async( + "https://contoso.sharepoint.com/sites/site1", + "CustomList", + PostItemInput() + ) + + assert exc_info.value.status_code == 400 + + @pytest.mark.asyncio + async def test_delete_item_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=403, + text='{"error": "Access denied"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.delete_item_async( + "https://contoso.sharepoint.com/sites/site1", + "CustomList", + 999 + ) + + assert exc_info.value.status_code == 403 + class TestSharingOperations: """Tests for sharing and permissions operations.""" @@ -626,6 +855,64 @@ async def test_grant_access(self, mock_token_provider): assert call_args[0][0] == "POST" assert result is None + @pytest.mark.asyncio + async def test_create_sharing_link_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=400, + text='{"error": "Invalid sharing parameters"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.create_sharing_link_async( + ItemPermissionCreateLinkBody(), + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "item123" + ) + + assert exc_info.value.status_code == 400 + + @pytest.mark.asyncio + async def test_grant_access_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=403, + text='{"error": "Insufficient permissions"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.grant_access_async( + ItemGrantAccessBody(), + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "item123" + ) + + assert exc_info.value.status_code == 403 + class TestCopyMoveOperations: """Tests for copy and move operations.""" @@ -706,7 +993,6 @@ async def test_copy_folder(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ): - from azure.connectors.sharepointonline import CopyFolderParameters input_data = CopyFolderParameters() result = await client.copy_folder_async( input_data, @@ -715,6 +1001,61 @@ async def test_copy_folder(self, mock_token_provider): assert result["success"] is True + @pytest.mark.asyncio + async def test_copy_file_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=404, + text='{"error": "Source file not found"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.copy_file_async( + dataset="https://contoso.sharepoint.com/sites/site1", + source="/Shared Documents/missing.docx", + destination="/Backup/missing.docx" + ) + + assert exc_info.value.status_code == 404 + + @pytest.mark.asyncio + async def test_move_file_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=400, + text='{"error": "Invalid destination"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.move_file_async( + MoveFileParameters(), + "https://contoso.sharepoint.com/sites/site1" + ) + + assert exc_info.value.status_code == 400 + class TestApprovalOperations: """Tests for approval operations.""" @@ -774,19 +1115,78 @@ async def test_set_approval_status(self, mock_token_provider): assert result["status"] == "approved" + @pytest.mark.asyncio + async def test_create_approval_request_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) -class TestDataClasses: - """Tests for data classes and type definitions.""" - - def test_create_file_input_creation(self): - """Test CreateFileInput dataclass creation.""" - input_data = CreateFileInput() - assert input_data is not None - - def test_update_file_input_creation(self): - """Test UpdateFileInput dataclass creation.""" - input_data = UpdateFileInput() - assert input_data is not None + mock_response = MockResponse( + status=400, + text='{"error": "Invalid approval type"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.create_approval_request_async( + CreateApprovalRequestInput(), + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "item123", + "invalid" + ) + + assert exc_info.value.status_code == 400 + + @pytest.mark.asyncio + async def test_set_approval_status_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=404, + text='{"error": "Item not found"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.set_approval_status_async( + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "missing_item", + "approved" + ) + + assert exc_info.value.status_code == 404 + + +class TestDataClasses: + """Tests for data classes and type definitions.""" + + def test_create_file_input_creation(self): + """Test CreateFileInput dataclass creation.""" + input_data = CreateFileInput() + assert input_data is not None + + def test_update_file_input_creation(self): + """Test UpdateFileInput dataclass creation.""" + input_data = UpdateFileInput() + assert input_data is not None def test_post_item_input_creation(self): """Test PostItemInput dataclass creation.""" @@ -911,3 +1311,692 @@ async def test_server_error_raises_exception(self, mock_token_provider): await client.get_all_tables_async("https://contoso.sharepoint.com/sites/site1") assert exc_info.value.status_code == 500 + + +class TestCheckInOutOperations: + """Tests for file check-in/check-out operations.""" + + @pytest.mark.asyncio + async def test_check_out_file(self, mock_token_provider): + """Test checking out a file.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text='') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + await client.check_out_file_async( + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "file123" + ) + + call_args = mock_send.call_args + assert call_args[0][0] == "POST" + assert "checkoutfile" in call_args[0][1] + + @pytest.mark.asyncio + async def test_check_in_file(self, mock_token_provider): + """Test checking in a file.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text='') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + input_data = FileCheckInParameters() + await client.check_in_file_async( + input_data, + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "file123" + ) + + call_args = mock_send.call_args + assert call_args[0][0] == "POST" + assert "checkinfile" in call_args[0][1] + + @pytest.mark.asyncio + async def test_discard_check_out(self, mock_token_provider): + """Test discarding a file check-out.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=200, text='') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + await client.discard_file_check_out_async( + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "file123" + ) + + call_args = mock_send.call_args + assert call_args[0][0] == "POST" + assert "discardfilecheckout" in call_args[0][1] + + @pytest.mark.asyncio + async def test_check_out_file_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=423, + text='{"error": "File is locked"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.check_out_file_async( + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "locked_file" + ) + + assert exc_info.value.status_code == 423 + + +class TestAttachmentOperations: + """Tests for attachment operations.""" + + @pytest.mark.asyncio + async def test_get_item_attachments(self, mock_token_provider): + """Test getting attachments for a list item.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='[{"id": "att1", "display_name": "doc.pdf"}]' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_item_attachments_async( + "https://contoso.sharepoint.com/sites/site1", + "CustomList", + "1" + ) + + assert isinstance(result, list) + + @pytest.mark.asyncio + async def test_create_attachment(self, mock_token_provider): + """Test creating an attachment.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=201, + text='{"id": "att123", "display_name": "newfile.pdf"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + input_data = CreateAttachmentInput() + result = await client.create_attachment_async( + input_data, + "https://contoso.sharepoint.com/sites/site1", + "CustomList", + "1", + "newfile.pdf" + ) + + call_args = mock_send.call_args + assert call_args[0][0] == "POST" + assert result["id"] == "att123" + + @pytest.mark.asyncio + async def test_delete_attachment(self, mock_token_provider): + """Test deleting an attachment.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=204, text='') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + await client.delete_attachment_async( + "https://contoso.sharepoint.com/sites/site1", + "CustomList", + "1", + "att123" + ) + + call_args = mock_send.call_args + assert call_args[0][0] == "DELETE" + + @pytest.mark.asyncio + async def test_get_attachment_content(self, mock_token_provider): + """Test getting attachment content.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + file_content = b"attachment binary content" + mock_response = MockResponse( + status=200, + text=file_content.decode('latin-1') + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_attachment_content_async( + "https://contoso.sharepoint.com/sites/site1", + "CustomList", + "1", + "att123" + ) + + assert result == file_content + + @pytest.mark.asyncio + async def test_create_attachment_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=413, + text='{"error": "Attachment too large"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.create_attachment_async( + CreateAttachmentInput(), + "https://contoso.sharepoint.com/sites/site1", + "CustomList", + "1", + "largefile.pdf" + ) + + assert exc_info.value.status_code == 413 + + +class TestSearchOperations: + """Tests for search operations.""" + + @pytest.mark.asyncio + async def test_search_for_user(self, mock_token_provider): + """Test searching for a user.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"display_name": "John Doe", "email": "john@contoso.com"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.search_for_user_async( + "https://contoso.sharepoint.com/sites/site1", + "CustomList", + "entity1", + "john" + ) + + assert result["display_name"] == "John Doe" + + @pytest.mark.asyncio + async def test_search_for_user_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=400, + text='{"error": "Multiple users found"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.search_for_user_async( + "https://contoso.sharepoint.com/sites/site1", + "CustomList", + "entity1", + "smith" + ) + + assert exc_info.value.status_code == 400 + + +class TestFileItemOperations: + """Tests for file item operations.""" + + @pytest.mark.asyncio + async def test_get_file_item(self, mock_token_provider): + """Test getting file item properties.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"Id": "file123", "Name": "document.docx", "Size": 1024}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_file_item_async( + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "file123" + ) + + assert result["Id"] == "file123" + + @pytest.mark.asyncio + async def test_patch_file_item(self, mock_token_provider): + """Test updating file item properties.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"Id": "file123", "Title": "Updated Title"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + input_data = PatchFileItemInput() + result = await client.patch_file_item_async( + input_data, + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "file123" + ) + + call_args = mock_send.call_args + assert call_args[0][0] == "PATCH" + assert result["Title"] == "Updated Title" + + @pytest.mark.asyncio + async def test_unshare_item(self, mock_token_provider): + """Test unsharing an item.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=204, text='') + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ) as mock_send: + await client.unshare_item_async( + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "item123" + ) + + call_args = mock_send.call_args + assert call_args[0][0] == "POST" + assert "unshare" in call_args[0][1] + + @pytest.mark.asyncio + async def test_get_file_item_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=404, + text='{"error": "File not found"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_file_item_async( + "https://contoso.sharepoint.com/sites/site1", + "Documents", + "missing_file" + ) + + assert exc_info.value.status_code == 404 + + +class TestMoveFolderOperations: + """Tests for move folder operations.""" + + @pytest.mark.asyncio + async def test_move_folder(self, mock_token_provider): + """Test moving a folder.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"Id": "folder123", "Path": "/Archive/OldFolder"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + input_data = MoveFolderParameters() + result = await client.move_folder_async( + input_data, + "https://contoso.sharepoint.com/sites/site1" + ) + + assert "/Archive/" in result["Path"] + + @pytest.mark.asyncio + async def test_move_folder_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=400, + text='{"error": "Invalid destination"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.move_folder_async( + MoveFolderParameters(), + "https://contoso.sharepoint.com/sites/site1" + ) + + assert exc_info.value.status_code == 400 + + +class TestTriggerOperations: + """Tests for trigger operations.""" + + @pytest.mark.asyncio + async def test_get_on_new_items(self, mock_token_provider): + """Test getting new items trigger.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"value": [{"Id": 1, "Title": "New Item"}]}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_on_new_items_async( + "https://contoso.sharepoint.com/sites/site1", + "CustomList" + ) + + assert "value" in result + + @pytest.mark.asyncio + async def test_get_on_updated_items(self, mock_token_provider): + """Test getting updated items trigger.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"value": [{"Id": 1, "Title": "Updated Item"}]}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_on_updated_items_async( + "https://contoso.sharepoint.com/sites/site1", + "CustomList" + ) + + assert "value" in result + + @pytest.mark.asyncio + async def test_get_on_new_items_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=404, + text='{"error": "List not found"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_on_new_items_async( + "https://contoso.sharepoint.com/sites/site1", + "MissingList" + ) + + assert exc_info.value.status_code == 404 + + +class TestByPathOperations: + """Tests for operations using file/folder paths.""" + + @pytest.mark.asyncio + async def test_get_file_metadata_by_path(self, mock_token_provider): + """Test getting file metadata by path.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"Id": "file123", "Name": "document.docx", ' + '"Path": "/Shared Documents/document.docx"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_file_metadata_by_path_async( + "https://contoso.sharepoint.com/sites/site1", + "/Shared Documents/document.docx" + ) + + assert result["Name"] == "document.docx" + + @pytest.mark.asyncio + async def test_get_file_content_by_path(self, mock_token_provider): + """Test getting file content by path.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + file_content = b"file binary content" + mock_response = MockResponse( + status=200, + text=file_content.decode('latin-1') + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_file_content_by_path_async( + "https://contoso.sharepoint.com/sites/site1", + "/Shared Documents/document.docx" + ) + + assert result == file_content + + @pytest.mark.asyncio + async def test_get_folder_metadata_by_path(self, mock_token_provider): + """Test getting folder metadata by path.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=200, + text='{"Id": "folder123", "Name": "Documents", "ItemCount": 10}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + result = await client.get_folder_metadata_by_path_async( + "https://contoso.sharepoint.com/sites/site1", + "/Shared Documents" + ) + + assert result["ItemCount"] == 10 + + @pytest.mark.asyncio + async def test_get_file_metadata_by_path_error_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SharepointonlineClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse( + status=404, + text='{"error": "File not found at path"}' + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_file_metadata_by_path_async( + "https://contoso.sharepoint.com/sites/site1", + "/Shared Documents/missing.docx" + ) + + assert exc_info.value.status_code == 404 diff --git a/tests/test_smtp.py b/tests/test_smtp.py index 4121044..6ca036f 100644 --- a/tests/test_smtp.py +++ b/tests/test_smtp.py @@ -15,6 +15,7 @@ from azure.connectors.sdk import ( ConnectorClientOptions, ManagedIdentityTokenProvider, + ConnectorException, ) from tests.conftest import MockResponse @@ -270,6 +271,33 @@ async def test_with_high_importance(self, mock_token_provider): ): await client.send_email_async(input=email_input) + @pytest.mark.asyncio + async def test_error_response_raises_exception(self, mock_token_provider): + """Test that error response raises ConnectorException.""" + client = SmtpClient( + "https://example.azure.com/connections/test", + token_provider=mock_token_provider + ) + + mock_response = MockResponse(status=400, text='{"error": "Invalid email format"}') + email_input = EmailV3( + from_="invalid", + to="recipient@contoso.com", + subject="Test", + body="Test body" + ) + + with patch.object( + client._http_client, + 'send_async', + new_callable=AsyncMock, + return_value=mock_response + ): + with pytest.raises(ConnectorException) as exc_info: + await client.send_email_async(input=email_input) + + assert exc_info.value.status_code == 400 + class TestDataClasses: """Tests for data class creation and attributes.""" diff --git a/tests/test_teams.py b/tests/test_teams.py index 0db35a6..2237d0d 100644 --- a/tests/test_teams.py +++ b/tests/test_teams.py @@ -12,11 +12,12 @@ CreateATeamInput, AddMemberToTeamInput, AddMemberToChannelInput, + NewChat, + HttpRequestInput, + WebhookChatMessageTriggerInput, + DynamicUserMessageWithOptionsSubscriptionRequest, CreateSectionInput, PostMessageToSelfRequest, - NewChat, - DynamicGetMessageDetailsSchema, - DynamicListMembersSchema, ) from azure.connectors.sdk import ConnectorClientOptions, ConnectorException from tests.conftest import MockResponse @@ -236,21 +237,17 @@ async def test_get_all_associated_teams_success(self, mock_token_provider): assert "value" in result - -class TestChannelOperations: - """Tests for channel operations.""" - @pytest.mark.asyncio - async def test_get_channels_for_group_success(self, mock_token_provider): - """Test successful retrieval of channels.""" + async def test_get_all_teams_error_response(self, mock_token_provider): + """Test that error response raises ConnectorException.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider ) mock_response = MockResponse( - status=200, - text='{"value": [{"id": "channel1", "displayName": "General"}]}' + status=401, + text='{"error": "Unauthorized"}' ) with patch.object( @@ -258,47 +255,19 @@ async def test_get_channels_for_group_success(self, mock_token_provider): 'send_async', new_callable=AsyncMock, return_value=mock_response - ) as mock_send: - result = await client.get_channels_for_group_async("group123") + ): + with pytest.raises(ConnectorException) as exc_info: + await client.get_all_associated_teams_async() - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/beta/groups/group123/channels" in call_args[0][1] - assert "value" in result + assert exc_info.value.status_code == 401 - @pytest.mark.asyncio - async def test_create_channel_success(self, mock_token_provider): - """Test successful channel creation.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - mock_response = MockResponse( - status=201, - text='{"id": "channel123", "displayName": "New Channel"}' - ) - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - input_data = CreateChannelInput( - display_name="New Channel", - description="Test Description" - ) - result = await client.create_channel_async(input_data, "group123") - - call_args = mock_send.call_args - assert call_args[0][0] == "POST" - assert "/beta/groups/group123/channels" in call_args[0][1] - assert result["id"] == "channel123" +class TestUserOperations: + """Tests for user operations.""" @pytest.mark.asyncio - async def test_get_channel_success(self, mock_token_provider): - """Test successful channel retrieval.""" + async def test_at_mention_user_success(self, mock_token_provider): + """Test successful @mention token retrieval.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider @@ -306,7 +275,7 @@ async def test_get_channel_success(self, mock_token_provider): mock_response = MockResponse( status=200, - text='{"id": "channel123", "displayName": "General"}' + text='{"id": "user123", "displayName": "John Doe", "mail": "john@example.com"}' ) with patch.object( @@ -315,69 +284,38 @@ async def test_get_channel_success(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ) as mock_send: - result = await client.get_channel_async("group123", "channel123") + result = await client.at_mention_user_async("user123") call_args = mock_send.call_args assert call_args[0][0] == "GET" - assert "/beta/teams/group123/channels/channel123" in call_args[0][1] - assert result["id"] == "channel123" + assert "/v1.0/users/user123" in call_args[0][1] + assert result["displayName"] == "John Doe" @pytest.mark.asyncio - async def test_get_all_channels_for_team_success(self, mock_token_provider): - """Test successful retrieval of all channels for a team.""" + async def test_at_mention_user_error(self, mock_token_provider): + """Test @mention user error handling.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider ) - mock_response = MockResponse( - status=200, - text='{"value": [{"id": "channel1", "displayName": "General"}]}' - ) + mock_response = MockResponse(status=404, text='{"error": "User not found"}') with patch.object( client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response - ) as mock_send: - result = await client.get_all_channels_for_team_async("group123") + ): + with pytest.raises(ConnectorException) as exc_info: + await client.at_mention_user_async("nonexistent") - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/beta/teams/group123/allChannels" in call_args[0][1] - assert "value" in result + assert exc_info.value.status_code == 404 class TestChatOperations: """Tests for chat operations.""" - @pytest.mark.asyncio - async def test_get_chats_success(self, mock_token_provider): - """Test successful retrieval of chats.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse( - status=200, - text='{"value": [{"id": "chat1", "topic": "Test Chat"}]}' - ) - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - result = await client.get_chats_async("oneOnOne", "Test") - - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/flowbot/actions/listchats/chattypes/oneOnOne/topic/Test" in call_args[0][1] - assert "value" in result - @pytest.mark.asyncio async def test_create_chat_success(self, mock_token_provider): """Test successful chat creation.""" @@ -388,7 +326,7 @@ async def test_create_chat_success(self, mock_token_provider): mock_response = MockResponse( status=201, - text='{"id": "chat123", "topic": "Test Chat"}' + text='{"id": "chat123", "chatType": "oneOnOne"}' ) with patch.object( @@ -397,7 +335,7 @@ async def test_create_chat_success(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ) as mock_send: - input_data = NewChat(topic="Test Chat", members="user1;user2") + input_data = NewChat() result = await client.create_chat_async(input_data) call_args = mock_send.call_args @@ -405,66 +343,34 @@ async def test_create_chat_success(self, mock_token_provider): assert "/beta/chats" in call_args[0][1] assert result["id"] == "chat123" - -class TestTagOperations: - """Tests for tag operations.""" - @pytest.mark.asyncio - async def test_get_tags_success(self, mock_token_provider): - """Test successful retrieval of tags.""" + async def test_create_chat_error(self, mock_token_provider): + """Test chat creation error handling.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider ) - mock_response = MockResponse( - status=200, - text='{"value": [{"id": "tag1", "displayName": "Test Tag"}]}' - ) + mock_response = MockResponse(status=400, text='{"error": "Bad Request"}') with patch.object( client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response - ) as mock_send: - result = await client.get_tags_async("group123") - - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/v1.0/teams/group123/tags" in call_args[0][1] - assert "value" in result - - @pytest.mark.asyncio - async def test_create_tag_success(self, mock_token_provider): - """Test successful tag creation.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) + ): + with pytest.raises(ConnectorException) as exc_info: + await client.create_chat_async(NewChat()) - mock_response = MockResponse( - status=201, - text='{"id": "tag123", "displayName": "New Tag"}' - ) + assert exc_info.value.status_code == 400 - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - input_data = CreateTagInput(display_name="New Tag") - result = await client.create_tag_async(input_data, "group123") - call_args = mock_send.call_args - assert call_args[0][0] == "POST" - assert "/v1.0/teams/group123/tags" in call_args[0][1] - assert result["id"] == "tag123" +class TestTeamCreationOperations: + """Tests for team creation operations.""" @pytest.mark.asyncio - async def test_add_member_to_tag_success(self, mock_token_provider): - """Test successful member addition to tag.""" + async def test_create_a_team_success(self, mock_token_provider): + """Test successful team creation.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider @@ -472,7 +378,7 @@ async def test_add_member_to_tag_success(self, mock_token_provider): mock_response = MockResponse( status=201, - text='{"user_id": "user123"}' + text='{"id": "team123", "displayName": "New Team"}' ) with patch.object( @@ -481,91 +387,46 @@ async def test_add_member_to_tag_success(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ) as mock_send: - input_data = AddMemberToTagInput(user_id="user123") - result = await client.add_member_to_tag_async(input_data, "group123", "tag123") + input_data = CreateATeamInput( + display_name="New Team", + description="A new team", + visibility="Private" + ) + result = await client.create_a_team_async(input_data) call_args = mock_send.call_args assert call_args[0][0] == "POST" - assert "/v1.0/teams/group123/tags/tag123/members" in call_args[0][1] - assert result["user_id"] == "user123" + assert "/beta/teams" in call_args[0][1] + assert result["displayName"] == "New Team" @pytest.mark.asyncio - async def test_get_tag_members_success(self, mock_token_provider): - """Test successful retrieval of tag members.""" + async def test_create_a_team_error(self, mock_token_provider): + """Test team creation error handling.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider ) - mock_response = MockResponse( - status=200, - text='{"value": [{"id": "member1", "displayName": "User 1"}]}' - ) + mock_response = MockResponse(status=403, text='{"error": "Forbidden"}') with patch.object( client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response - ) as mock_send: - result = await client.get_tag_members_async("group123", "tag123") - - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/v1.0/teams/group123/tags/tag123/members" in call_args[0][1] - assert "value" in result - - @pytest.mark.asyncio - async def test_delete_tag_member_success(self, mock_token_provider): - """Test successful tag member deletion.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse(status=204, text='') - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - await client.delete_tag_member_async("group123", "tag123", "member123") - - call_args = mock_send.call_args - assert call_args[0][0] == "DELETE" - assert "/v1.0/teams/group123/tags/tag123/members/member123" in call_args[0][1] - - @pytest.mark.asyncio - async def test_delete_tag_success(self, mock_token_provider): - """Test successful tag deletion.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse(status=204, text='') - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - await client.delete_tag_async("group123", "tag123") + ): + with pytest.raises(ConnectorException) as exc_info: + await client.create_a_team_async(CreateATeamInput()) - call_args = mock_send.call_args - assert call_args[0][0] == "DELETE" - assert "/v1.0/teams/group123/tags/tag123" in call_args[0][1] + assert exc_info.value.status_code == 403 -class TestMessageOperations: - """Tests for message operations.""" +class TestMembershipTriggers: + """Tests for membership trigger operations.""" @pytest.mark.asyncio - async def test_get_messages_from_channel_success(self, mock_token_provider): - """Test successful retrieval of channel messages.""" + async def test_on_group_membership_add_success(self, mock_token_provider): + """Test successful membership add trigger.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider @@ -573,7 +434,7 @@ async def test_get_messages_from_channel_success(self, mock_token_provider): mock_response = MockResponse( status=200, - text='{"value": [{"id": "msg1", "body": {"content": "Hello"}}]}' + text='{"value": [{"id": "member1", "displayName": "New Member"}]}' ) with patch.object( @@ -582,16 +443,17 @@ async def test_get_messages_from_channel_success(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ) as mock_send: - result = await client.get_messages_from_channel_async("group123", "channel123") + result = await client.on_group_membership_add_async(group_id="team-123") call_args = mock_send.call_args assert call_args[0][0] == "GET" - assert "/beta/teams/group123/channels/channel123/messages" in call_args[0][1] + assert "/trigger/v1.0/groups/delta" in call_args[0][1] + assert "groupId=team-123" in call_args[0][1] assert "value" in result @pytest.mark.asyncio - async def test_get_message_details_success(self, mock_token_provider): - """Test successful retrieval of message details.""" + async def test_on_group_membership_add_with_select(self, mock_token_provider): + """Test membership add trigger with select parameter.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider @@ -599,7 +461,7 @@ async def test_get_message_details_success(self, mock_token_provider): mock_response = MockResponse( status=200, - text='{"id": "msg123", "body": {"content": "Message content"}}' + text='{"value": [{"displayName": "New Member"}]}' ) with patch.object( @@ -608,47 +470,18 @@ async def test_get_message_details_success(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ) as mock_send: - input_data = DynamicGetMessageDetailsSchema() - result = await client.get_message_details_async(input_data, "msg123", "channel") - - call_args = mock_send.call_args - assert call_args[0][0] == "POST" - assert "/beta/teams/messages/msg123/messageType/channel" in call_args[0][1] - assert result["id"] == "msg123" - - @pytest.mark.asyncio - async def test_list_replies_to_message_success(self, mock_token_provider): - """Test successful listing of message replies.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse( - status=200, - text='{"value": [{"id": "reply1", "body": {"content": "Reply"}}]}' - ) - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - result = await client.list_replies_to_message_async( - "group123", "channel123", "msg123" + result = await client.on_group_membership_add_async( + group_id="team-123", + select="displayName" ) call_args = mock_send.call_args - assert call_args[0][0] == "GET" - path = call_args[0][1] - assert "/v1.0/teams/group123/channels/channel123" in path - assert "/messages/msg123/replies" in path + assert "$select=" in call_args[0][1] assert "value" in result @pytest.mark.asyncio - async def test_list_replies_with_top_parameter(self, mock_token_provider): - """Test listing replies with top parameter.""" + async def test_on_group_membership_removal_success(self, mock_token_provider): + """Test successful membership removal trigger.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider @@ -656,7 +489,7 @@ async def test_list_replies_with_top_parameter(self, mock_token_provider): mock_response = MockResponse( status=200, - text='{"value": [{"id": "reply1", "body": {"content": "Reply"}}]}' + text='{"value": [{"id": "member1", "displayName": "Removed Member"}]}' ) with patch.object( @@ -665,52 +498,42 @@ async def test_list_replies_with_top_parameter(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ) as mock_send: - result = await client.list_replies_to_message_async( - "group123", "channel123", "msg123", top="10" - ) + result = await client.on_group_membership_removal_async(group_id="team-123") call_args = mock_send.call_args assert call_args[0][0] == "GET" - path = call_args[0][1] - assert "/v1.0/teams/group123/channels/channel123" in path - assert "/messages/msg123/replies" in path - assert "$top=10" in path + assert "/trigger/v1.0/groups/removal" in call_args[0][1] + assert "groupId=team-123" in call_args[0][1] assert "value" in result @pytest.mark.asyncio - async def test_post_message_to_self_success(self, mock_token_provider): - """Test successful posting message to self.""" + async def test_on_group_membership_removal_error(self, mock_token_provider): + """Test membership removal trigger error handling.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider ) - mock_response = MockResponse( - status=201, - text='{"id": "message123", "body": {"content": "Test message"}}' - ) + mock_response = MockResponse(status=401, text='{"error": "Unauthorized"}') with patch.object( client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response - ) as mock_send: - input_data = PostMessageToSelfRequest(body={"content": "Test message"}) - result = await client.post_message_to_self_async(input_data) + ): + with pytest.raises(ConnectorException) as exc_info: + await client.on_group_membership_removal_async(group_id="team-123") - call_args = mock_send.call_args - assert call_args[0][0] == "POST" - assert "/v1.0/chats/48:notes/messages" in call_args[0][1] - assert result["id"] == "message123" + assert exc_info.value.status_code == 401 -class TestMemberOperations: - """Tests for member operations.""" +class TestHttpRequestOperations: + """Tests for HTTP request operations.""" @pytest.mark.asyncio - async def test_list_members_success(self, mock_token_provider): - """Test successful listing of members.""" + async def test_http_request_success(self, mock_token_provider): + """Test successful HTTP request.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider @@ -718,7 +541,7 @@ async def test_list_members_success(self, mock_token_provider): mock_response = MockResponse( status=200, - text='{"value": [{"id": "member1", "displayName": "User 1"}]}' + text='{"data": "response data"}' ) with patch.object( @@ -727,182 +550,23 @@ async def test_list_members_success(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ) as mock_send: - input_data = DynamicListMembersSchema() - result = await client.list_members_async(input_data, "channel") + input_data = HttpRequestInput() + result = await client.http_request_async(input_data) call_args = mock_send.call_args assert call_args[0][0] == "POST" - assert "/v1.0/teams/listmembers/threadType/channel" in call_args[0][1] - assert "value" in result - - @pytest.mark.asyncio - async def test_list_team_members_success(self, mock_token_provider): - """Test successful listing of team members.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse( - status=200, - text='{"value": [{"id": "member1", "displayName": "User 1"}]}' - ) - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - result = await client.list_team_members_async("team123") - - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/v1.0/teams/team123/members" in call_args[0][1] - assert "value" in result + assert "/httprequest" in call_args[0][1] + assert result["data"] == "response data" @pytest.mark.asyncio - async def test_add_member_to_team_success(self, mock_token_provider): - """Test successful addition of member to team.""" + async def test_http_request_error(self, mock_token_provider): + """Test HTTP request error handling.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider ) - mock_response = MockResponse(status=204, text='') - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - input_data = AddMemberToTeamInput(user_id="user@example.com", owner=False) - await client.add_member_to_team_async(input_data, "team123") - - call_args = mock_send.call_args - assert call_args[0][0] == "POST" - assert "/v1.0/teams/team123/members" in call_args[0][1] - - @pytest.mark.asyncio - async def test_remove_member_from_team_success(self, mock_token_provider): - """Test successful removal of member from team.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse(status=204, text='') - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - await client.remove_member_from_team_async("team123", "membership123") - - call_args = mock_send.call_args - assert call_args[0][0] == "DELETE" - assert "/v1.0/teams/team123/members/membership123" in call_args[0][1] - - -class TestTriggerOperations: - """Tests for trigger operations.""" - - @pytest.mark.asyncio - async def test_on_new_channel_message_success(self, mock_token_provider): - """Test successful new channel message trigger.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse( - status=200, - text='{"value": [{"id": "msg1", "body": {"content": "New message"}}]}' - ) - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - result = await client.on_new_channel_message_async("group123", "channel123") - - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/trigger/beta/teams/group123/channels/channel123/messages" in call_args[0][1] - assert "value" in result - - @pytest.mark.asyncio - async def test_on_new_channel_message_with_top(self, mock_token_provider): - """Test new channel message trigger with top parameter.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse( - status=200, - text='{"value": [{"id": "msg1", "body": {"content": "New message"}}]}' - ) - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - result = await client.on_new_channel_message_async("group123", "channel123", top="5") - - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/trigger/beta/teams/group123/channels/channel123/messages" in call_args[0][1] - assert "$top=5" in call_args[0][1] - assert "value" in result - - -class TestTeamOperations: - """Tests for team operations.""" - - @pytest.mark.asyncio - async def test_get_team_success(self, mock_token_provider): - """Test successful retrieval of a team.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse( - status=200, - text='{"id": "team123", "displayName": "Test Team", "description": "A test team"}' - ) - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - result = await client.get_team_async("team123") - - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/beta/teams/team123" in call_args[0][1] - assert result["id"] == "team123" - assert result["displayName"] == "Test Team" - - @pytest.mark.asyncio - async def test_get_team_error(self, mock_token_provider): - """Test get team error handling.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse(status=404, text='{"error": "Team not found"}') + mock_response = MockResponse(status=500, text='{"error": "Internal Server Error"}') with patch.object( client._http_client, @@ -911,84 +575,23 @@ async def test_get_team_error(self, mock_token_provider): return_value=mock_response ): with pytest.raises(ConnectorException) as exc_info: - await client.get_team_async("nonexistent") - - assert exc_info.value.status_code == 404 + await client.http_request_async(HttpRequestInput()) + assert exc_info.value.status_code == 500 -class TestOnlineMeetingOperations: - """Tests for online meeting operations.""" - - @pytest.mark.asyncio - async def test_get_online_meeting_success(self, mock_token_provider): - """Test successful retrieval of an online meeting.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse( - status=200, - text='{"id": "meeting123", "subject": "Test Meeting", ' - '"joinWebUrl": "https://teams.microsoft.com/l/meetup/..."}' - ) - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - result = await client.get_online_meeting_async("meetingId", "meeting123") - - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/v1.0/me/onlineMeetings/lookup" in call_args[0][1] - assert "lookupType=meetingId" in call_args[0][1] - assert result["id"] == "meeting123" - - -class TestSectionOperations: - """Tests for section operations.""" - - @pytest.mark.asyncio - async def test_list_sections_success(self, mock_token_provider): - """Test successful listing of sections.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse( - status=200, - text='{"value": [{"id": "section1", "displayName": "My Section"}]}' - ) - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - result = await client.list_sections_async() - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/beta/me/teamwork/sections" in call_args[0][1] - assert "value" in result +class TestWebhookOperations: + """Tests for webhook and subscription operations.""" @pytest.mark.asyncio - async def test_create_section_success(self, mock_token_provider): - """Test successful section creation.""" + async def test_webhook_chat_message_trigger_success(self, mock_token_provider): + """Test successful webhook chat message trigger.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider ) - mock_response = MockResponse( - status=201, - text='{"id": "section123", "displayName": "New Section"}' - ) + mock_response = MockResponse(status=200, text='') with patch.object( client._http_client, @@ -996,87 +599,41 @@ async def test_create_section_success(self, mock_token_provider): new_callable=AsyncMock, return_value=mock_response ) as mock_send: - input_data = CreateSectionInput(display_name="New Section") - result = await client.create_section_async(input_data) + input_data = WebhookChatMessageTriggerInput() + await client.webhook_chat_message_trigger_async(input_data) call_args = mock_send.call_args assert call_args[0][0] == "POST" - assert "/beta/me/teamwork/sections" in call_args[0][1] - assert result["id"] == "section123" - - -class TestAdhocCallOperations: - """Tests for ad-hoc call recording and transcript operations.""" + assert "/beta/subscriptions/chatmessagetrigger" in call_args[0][1] @pytest.mark.asyncio - async def test_get_all_adhoc_call_recordings_success(self, mock_token_provider): - """Test successful retrieval of ad-hoc call recordings.""" + async def test_webhook_chat_message_trigger_error(self, mock_token_provider): + """Test webhook chat message trigger error handling.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider ) - mock_response = MockResponse( - status=200, - text='{"value": [{"id": "recording1", "createdDateTime": "2024-01-15T10:00:00Z"}]}' - ) - - with patch.object( - client._http_client, - 'send_async', - new_callable=AsyncMock, - return_value=mock_response - ) as mock_send: - result = await client.get_all_adhoc_call_recordings_async() - - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/v1.0/me/adhocCalls/getAllRecordings" in call_args[0][1] - assert "value" in result - - @pytest.mark.asyncio - async def test_get_all_adhoc_call_recordings_with_params(self, mock_token_provider): - """Test retrieval of ad-hoc call recordings with query parameters.""" - client = TeamsClient( - "https://example.azure.com/connections/test", - token_provider=mock_token_provider - ) - - mock_response = MockResponse( - status=200, - text='{"value": []}' - ) + mock_response = MockResponse(status=400, text='{"error": "Bad Request"}') with patch.object( client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response - ) as mock_send: - await client.get_all_adhoc_call_recordings_async( - start_date_time="2024-01-01T00:00:00Z", - end_date_time="2024-01-31T23:59:59Z", - top="10" - ) - - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "startDateTime=" in call_args[0][1] - assert "endDateTime=" in call_args[0][1] - assert "$top=10" in call_args[0][1] + ): + with pytest.raises(ConnectorException): + await client.webhook_chat_message_trigger_async(WebhookChatMessageTriggerInput()) @pytest.mark.asyncio - async def test_get_all_adhoc_call_transcripts_success(self, mock_token_provider): - """Test successful retrieval of ad-hoc call transcripts.""" + async def test_subscribe_user_message_with_options_success(self, mock_token_provider): + """Test successful user message subscription.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider ) - mock_response = MockResponse( - status=200, - text='{"value": [{"id": "transcript1", "createdDateTime": "2024-01-15T10:00:00Z"}]}' - ) + mock_response = MockResponse(status=200, text='') with patch.object( client._http_client, @@ -1084,43 +641,33 @@ async def test_get_all_adhoc_call_transcripts_success(self, mock_token_provider) new_callable=AsyncMock, return_value=mock_response ) as mock_send: - result = await client.get_all_adhoc_call_transcripts_async() + input_data = DynamicUserMessageWithOptionsSubscriptionRequest() + await client.subscribe_user_message_with_options_async(input_data) call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "/v1.0/me/adhocCalls/getAllTranscripts" in call_args[0][1] - assert "value" in result + assert call_args[0][0] == "POST" + assert "/flowbot/actions/messagewithoptions" in call_args[0][1] @pytest.mark.asyncio - async def test_get_all_adhoc_call_transcripts_with_params(self, mock_token_provider): - """Test retrieval of ad-hoc call transcripts with query parameters.""" + async def test_subscribe_user_message_with_options_error(self, mock_token_provider): + """Test user message subscription error handling.""" client = TeamsClient( "https://example.azure.com/connections/test", token_provider=mock_token_provider ) - mock_response = MockResponse( - status=200, - text='{"value": []}' - ) + mock_response = MockResponse(status=403, text='{"error": "Forbidden"}') with patch.object( client._http_client, 'send_async', new_callable=AsyncMock, return_value=mock_response - ) as mock_send: - await client.get_all_adhoc_call_transcripts_async( - start_date_time="2024-01-01T00:00:00Z", - end_date_time="2024-01-31T23:59:59Z", - top="10" - ) - - call_args = mock_send.call_args - assert call_args[0][0] == "GET" - assert "startDateTime=" in call_args[0][1] - assert "endDateTime=" in call_args[0][1] - assert "$top=10" in call_args[0][1] + ): + with pytest.raises(ConnectorException): + await client.subscribe_user_message_with_options_async( + DynamicUserMessageWithOptionsSubscriptionRequest() + ) class TestDataClasses: