diff --git a/aioazuredevops/client.py b/aioazuredevops/client.py index 6a4c358..273b9f7 100644 --- a/aioazuredevops/client.py +++ b/aioazuredevops/client.py @@ -16,6 +16,11 @@ VersionControl, ) from .models.iteration import Iteration, IterationAttributes +from .models.iteration_work_item import ( + IterationWorkItemsResult, + WorkItemRelation, + WorkItemRelationTarget, +) from .models.wiql import WIQLColumn, WIQLResult, WIQLWorkItem from .models.work_item import ( WorkItem, @@ -370,6 +375,36 @@ async def get_iteration( url=iteration["url"], ) + async def get_iteration_work_items( + self, + organization: str, + project: str, + iteration_id: str, + ) -> IterationWorkItemsResult | None: + """Get Azure DevOps iteration work items.""" + response: aiohttp.ClientResponse = await self._get( + f"{DEFAULT_BASE_URL}/{organization}/{project}/_apis/work/teamsettings/iterations/{iteration_id}/workitems?api-version={DEFAULT_API_VERSION}" + ) + if response.status != 200: + return None + if (data := await response.json()) is None: + return None + + return IterationWorkItemsResult( + work_item_relations=[ + WorkItemRelation( + rel=work_item_relation.get("rel", None), + source=work_item_relation.get("source", None), + target=WorkItemRelationTarget( + id=work_item_relation["target"]["id"], + url=work_item_relation["target"]["url"], + ), + ) + for work_item_relation in data["workItemRelations"] + ], + url=data["url"], + ) + async def get_work_item_ids_from_wiql( self, organization: str, diff --git a/aioazuredevops/models/iteration_work_item.py b/aioazuredevops/models/iteration_work_item.py new file mode 100644 index 0000000..ee6d207 --- /dev/null +++ b/aioazuredevops/models/iteration_work_item.py @@ -0,0 +1,29 @@ +"""Model for Iteration Work Item.""" + +from dataclasses import dataclass +from typing import Any + + +@dataclass +class WorkItemRelationTarget: + """Work item relation target.""" + + id: int + url: str + + +@dataclass +class WorkItemRelation: + """Work item relation.""" + + rel: Any + source: Any + target: WorkItemRelationTarget + + +@dataclass +class IterationWorkItemsResult: + """Iteration work items result.""" + + work_item_relations: list[WorkItemRelation] + url: str diff --git a/tests/__init__.py b/tests/__init__.py index 6567bcc..fda77d8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -100,6 +100,13 @@ "value": [RESPONSE_JSON_DEVOPS_ITERATION], } +RESPONSE_JSON_DEVOPS_ITERATION_WORK_ITEMS: Final[dict] = { + "workItemRelations": [ + {"rel": None, "source": None, "target": {"id": 1, "url": "testurl"}} + ], + "url": "testurl", +} + RESPONSE_JSON_DEVOPS_WIQL_RESULT: Final[dict] = { "queryType": "testqueryType", "queryResultType": "testqueryResultType", diff --git a/tests/__snapshots__/test_client.ambr b/tests/__snapshots__/test_client.ambr index d191534..e8060fd 100644 --- a/tests/__snapshots__/test_client.ambr +++ b/tests/__snapshots__/test_client.ambr @@ -10,6 +10,9 @@ # name: test_get_iteration[iteration] Iteration(id='abc123', name='testname', path='testname\\Sprint 1', attributes=IterationAttributes(start_date='2021-01-01T00:00:00Z', finish_date='2021-01-31T00:00:00Z', time_frame='current'), url='testurl') # --- +# name: test_get_iteration_work_items[iteration_work_items] + IterationWorkItemsResult(work_item_relations=[WorkItemRelation(rel=None, source=None, target=WorkItemRelationTarget(id=1, url='testurl'))], url='testurl') +# --- # name: test_get_iterations[iterations] list([ Iteration(id='abc123', name='testname', path='testname\\Sprint 1', attributes=IterationAttributes(start_date='2021-01-01T00:00:00Z', finish_date='2021-01-31T00:00:00Z', time_frame='current'), url='testurl'), diff --git a/tests/conftest.py b/tests/conftest.py index cf1ad6e..cf5ae33 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,7 @@ RESPONSE_JSON_DEVOPS_BUILD, RESPONSE_JSON_DEVOPS_BUILDS, RESPONSE_JSON_DEVOPS_ITERATION, + RESPONSE_JSON_DEVOPS_ITERATION_WORK_ITEMS, RESPONSE_JSON_DEVOPS_ITERATIONS, RESPONSE_JSON_DEVOPS_PROJECT, RESPONSE_JSON_DEVOPS_WIQL_RESULT, @@ -88,6 +89,12 @@ def mock_aioresponse(): status=200, repeat=True, ) + mocker.get( + f"{DEFAULT_BASE_URL}/{ORGANIZATION}/{PROJECT}/_apis/work/teamsettings/iterations/abc123/workitems?api-version={DEFAULT_API_VERSION}", + payload=RESPONSE_JSON_DEVOPS_ITERATION_WORK_ITEMS, + status=200, + repeat=True, + ) mocker.post( f"{DEFAULT_BASE_URL}/{ORGANIZATION}/{PROJECT}/_apis/wit/wiql?api-version={DEFAULT_API_VERSION}", payload=RESPONSE_JSON_DEVOPS_WIQL_RESULT, diff --git a/tests/test_client.py b/tests/test_client.py index 60241e1..f6aaa7f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -301,6 +301,53 @@ async def test_get_iteration( assert empty_iteration is None +@pytest.mark.asyncio +async def test_get_iteration_work_items( + devops_client: DevOpsClient, + mock_aioresponse: aioresponses, + snapshot: SnapshotAssertion, +) -> None: + """Test the get_iteration_work_items method.""" + iteration_work_items = await devops_client.get_iteration_work_items( + organization=ORGANIZATION, + project=PROJECT, + iteration_id="abc123", + ) + + assert iteration_work_items == snapshot( + name="iteration_work_items", + ) + + # Test with bad request + mock_aioresponse.get( + f"{DEFAULT_BASE_URL}/{ORGANIZATION}/{BAD_PROJECT_NAME}/_apis/work/teamsettings/iterations/abc123/workitems?api-version={DEFAULT_API_VERSION}", + status=400, + ) + + bad_iteration_work_items = await devops_client.get_iteration_work_items( + organization=ORGANIZATION, + project=BAD_PROJECT_NAME, + iteration_id="abc123", + ) + + assert bad_iteration_work_items is None + + # Test with empty response + mock_aioresponse.get( + f"{DEFAULT_BASE_URL}/{ORGANIZATION}/{EMPTY_PROJECT_NAME}/_apis/work/teamsettings/iterations/abc123/workitems?api-version={DEFAULT_API_VERSION}", + payload=None, + status=200, + ) + + empty_iteration_work_items = await devops_client.get_iteration_work_items( + organization=ORGANIZATION, + project=EMPTY_PROJECT_NAME, + iteration_id="abc123", + ) + + assert empty_iteration_work_items is None + + @pytest.mark.asyncio async def test_get_work_items_ids( devops_client: DevOpsClient,