diff --git a/erclient/client.py b/erclient/client.py index 49c69d2..01bd794 100644 --- a/erclient/client.py +++ b/erclient/client.py @@ -1054,6 +1054,84 @@ def get_sources(self, page_size=100): def get_users(self): return self._get('users') + # -- Mapping / Spatial Feature Methods -- + + def get_features(self): + """Get a list of features (GeoJSON).""" + return self._get('features') + + def get_feature(self, feature_id): + """Get a single feature by ID (GeoJSON).""" + return self._get(f'feature/{feature_id}') + + def get_featuresets(self): + """Get a list of feature sets.""" + return self._get('featureset') + + def get_featureset(self, featureset_id): + """Get a single feature set by ID (GeoJSON).""" + return self._get(f'featureset/{featureset_id}') + + def get_maps(self): + """Get a list of maps.""" + return self._get('maps') + + def get_layers(self): + """Get a list of map layers.""" + return self._get('layers') + + def get_layer(self, layer_id): + """Get a single map layer by ID.""" + return self._get(f'layer/{layer_id}') + + def get_featureclasses(self): + """Get a list of spatial feature types (feature classes).""" + return self._get('featureclass') + + def get_spatialfeaturegroups(self): + """Get a list of spatial feature groups.""" + return self._get('spatialfeaturegroup') + + def get_spatialfeaturegroup(self, group_id): + """Get a single spatial feature group by ID.""" + return self._get(f'spatialfeaturegroup/{group_id}') + + def post_spatialfeaturegroup(self, data): + """Create a new spatial feature group.""" + self.logger.debug('Posting spatial feature group: %s', data) + return self._post('spatialfeaturegroup', payload=data) + + def patch_spatialfeaturegroup(self, group_id, data): + """Update a spatial feature group.""" + self.logger.debug('Patching spatial feature group %s: %s', group_id, data) + return self._patch(f'spatialfeaturegroup/{group_id}', payload=data) + + def delete_spatialfeaturegroup(self, group_id): + """Delete a spatial feature group.""" + return self._delete(f'spatialfeaturegroup/{group_id}/') + + def get_spatialfeatures(self): + """Get a list of spatial features.""" + return self._get('spatialfeature') + + def get_spatialfeature(self, feature_id): + """Get a single spatial feature by ID.""" + return self._get(f'spatialfeature/{feature_id}') + + def post_spatialfeature(self, data): + """Create a new spatial feature.""" + self.logger.debug('Posting spatial feature: %s', data) + return self._post('spatialfeature', payload=data) + + def patch_spatialfeature(self, feature_id, data): + """Update a spatial feature.""" + self.logger.debug('Patching spatial feature %s: %s', feature_id, data) + return self._patch(f'spatialfeature/{feature_id}', payload=data) + + def delete_spatialfeature(self, feature_id): + """Delete a spatial feature.""" + return self._delete(f'spatialfeature/{feature_id}/') + class AsyncERClient(object): """ @@ -1603,15 +1681,96 @@ async def get_source_assignments(self, subject_ids: List[str] = None, source_ids async def get_feature_group(self, feature_group_id: str): """ - Get a feature group by id + Get a feature group by id. - Args: - feature_group_id (int): id of the feature group - - Returns: - dict: feature group data + .. deprecated:: 1.x + Use :meth:`get_spatialfeaturegroup` instead; the name matches the + DAS API path ``spatialfeaturegroup``. """ - return await self._get(f"spatialfeaturegroup/{feature_group_id}", params={}) + warnings.warn( + "get_feature_group() is deprecated; use get_spatialfeaturegroup() instead.", + DeprecationWarning, + stacklevel=2, + ) + return await self.get_spatialfeaturegroup(feature_group_id) + + # -- Mapping / Spatial Feature Methods -- + + async def get_features(self): + """Get a list of features (GeoJSON).""" + return await self._get('features') + + async def get_feature(self, feature_id): + """Get a single feature by ID (GeoJSON).""" + return await self._get(f'feature/{feature_id}') + + async def get_featuresets(self): + """Get a list of feature sets.""" + return await self._get('featureset') + + async def get_featureset(self, featureset_id): + """Get a single feature set by ID (GeoJSON).""" + return await self._get(f'featureset/{featureset_id}') + + async def get_maps(self): + """Get a list of maps.""" + return await self._get('maps') + + async def get_layers(self): + """Get a list of map layers.""" + return await self._get('layers') + + async def get_layer(self, layer_id): + """Get a single map layer by ID.""" + return await self._get(f'layer/{layer_id}') + + async def get_featureclasses(self): + """Get a list of spatial feature types (feature classes).""" + return await self._get('featureclass') + + async def get_spatialfeaturegroups(self): + """Get a list of spatial feature groups.""" + return await self._get('spatialfeaturegroup') + + async def get_spatialfeaturegroup(self, group_id): + """Get a single spatial feature group by ID.""" + return await self._get(f'spatialfeaturegroup/{group_id}') + + async def post_spatialfeaturegroup(self, data): + """Create a new spatial feature group.""" + self.logger.debug(f'Posting spatial feature group: {data}') + return await self._post('spatialfeaturegroup', payload=data) + + async def patch_spatialfeaturegroup(self, group_id, data): + """Update a spatial feature group.""" + self.logger.debug(f'Patching spatial feature group {group_id}: {data}') + return await self._patch(f'spatialfeaturegroup/{group_id}', payload=data) + + async def delete_spatialfeaturegroup(self, group_id): + """Delete a spatial feature group.""" + return await self._delete(f'spatialfeaturegroup/{group_id}/') + + async def get_spatialfeatures(self): + """Get a list of spatial features.""" + return await self._get('spatialfeature') + + async def get_spatialfeature(self, feature_id): + """Get a single spatial feature by ID.""" + return await self._get(f'spatialfeature/{feature_id}') + + async def post_spatialfeature(self, data): + """Create a new spatial feature.""" + self.logger.debug(f'Posting spatial feature: {data}') + return await self._post('spatialfeature', payload=data) + + async def patch_spatialfeature(self, feature_id, data): + """Update a spatial feature.""" + self.logger.debug(f'Patching spatial feature {feature_id}: {data}') + return await self._patch(f'spatialfeature/{feature_id}', payload=data) + + async def delete_spatialfeature(self, feature_id): + """Delete a spatial feature.""" + return await self._delete(f'spatialfeature/{feature_id}/') async def _get_data(self, endpoint, params, batch_size=0): if "page" not in params: # Use cursor paginator unless the user has specified a page diff --git a/tests/async_client/test_mapping_spatial.py b/tests/async_client/test_mapping_spatial.py new file mode 100644 index 0000000..7feaef9 --- /dev/null +++ b/tests/async_client/test_mapping_spatial.py @@ -0,0 +1,454 @@ +import httpx +import pytest +import respx + +from erclient import ERClientNotFound, ERClientPermissionDenied + + +# -- Fixtures -- + +@pytest.fixture +def feature_response(): + return { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [36.79, -1.29]}, + "properties": {"name": "Ranger Post Alpha", "id": "fp-001"}, + } + + +@pytest.fixture +def features_list_response(feature_response): + return [feature_response] + + +@pytest.fixture +def featureset_response(): + return { + "id": "fs-001", + "name": "Ranger Posts", + "type": "FeatureCollection", + "features": [], + } + + +@pytest.fixture +def featuresets_list_response(featureset_response): + return [featureset_response] + + +@pytest.fixture +def map_response(): + return { + "id": "map-001", + "title": "Park Overview", + "layers": [], + } + + +@pytest.fixture +def maps_list_response(map_response): + return [map_response] + + +@pytest.fixture +def layer_response(): + return { + "id": "layer-001", + "title": "Roads", + "type": "geojson", + } + + +@pytest.fixture +def layers_list_response(layer_response): + return [layer_response] + + +@pytest.fixture +def featureclass_response(): + return { + "id": "fc-001", + "name": "boundary", + "display": "Boundary", + } + + +@pytest.fixture +def featureclasses_list_response(featureclass_response): + return [featureclass_response] + + +@pytest.fixture +def spatialfeaturegroup_payload(): + return {"name": "Protected Areas", "description": "All protected areas"} + + +@pytest.fixture +def spatialfeaturegroup_response(): + return { + "id": "sfg-001", + "name": "Protected Areas", + "description": "All protected areas", + "is_visible": True, + } + + +@pytest.fixture +def spatialfeaturegroups_list_response(spatialfeaturegroup_response): + return [spatialfeaturegroup_response] + + +@pytest.fixture +def spatialfeature_payload(): + return { + "name": "Nairobi National Park", + "type": "Polygon", + "geojson": { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [[[36.8, -1.3], [36.9, -1.3], [36.9, -1.4], [36.8, -1.4], [36.8, -1.3]]], + }, + }, + } + + +@pytest.fixture +def spatialfeature_response(): + return { + "id": "sf-001", + "name": "Nairobi National Park", + "type": "Polygon", + "geojson": { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [[[36.8, -1.3], [36.9, -1.3], [36.9, -1.4], [36.8, -1.4], [36.8, -1.3]]], + }, + }, + } + + +@pytest.fixture +def spatialfeatures_list_response(spatialfeature_response): + return [spatialfeature_response] + + +# -- Read-only endpoint tests -- + +@pytest.mark.asyncio +async def test_get_features(er_client, features_list_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("features") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": features_list_response}) + result = await er_client.get_features() + assert route.called + assert result == features_list_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_feature(er_client, feature_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("feature/fp-001") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": feature_response}) + result = await er_client.get_feature("fp-001") + assert route.called + assert result == feature_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_feature_not_found(er_client): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("feature/nonexistent") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.get_feature("nonexistent") + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_featuresets(er_client, featuresets_list_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("featureset") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": featuresets_list_response}) + result = await er_client.get_featuresets() + assert route.called + assert result == featuresets_list_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_featureset(er_client, featureset_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("featureset/fs-001") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": featureset_response}) + result = await er_client.get_featureset("fs-001") + assert route.called + assert result == featureset_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_maps(er_client, maps_list_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("maps") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": maps_list_response}) + result = await er_client.get_maps() + assert route.called + assert result == maps_list_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_layers(er_client, layers_list_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("layers") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": layers_list_response}) + result = await er_client.get_layers() + assert route.called + assert result == layers_list_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_layer(er_client, layer_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("layer/layer-001") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": layer_response}) + result = await er_client.get_layer("layer-001") + assert route.called + assert result == layer_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_featureclasses(er_client, featureclasses_list_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("featureclass") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": featureclasses_list_response}) + result = await er_client.get_featureclasses() + assert route.called + assert result == featureclasses_list_response + await er_client.close() + + +# -- Spatial Feature Group CRUD tests -- + +@pytest.mark.asyncio +async def test_get_spatialfeaturegroups(er_client, spatialfeaturegroups_list_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeaturegroup") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeaturegroups_list_response}) + result = await er_client.get_spatialfeaturegroups() + assert route.called + assert result == spatialfeaturegroups_list_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_spatialfeaturegroup(er_client, spatialfeaturegroup_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeaturegroup/sfg-001") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeaturegroup_response}) + result = await er_client.get_spatialfeaturegroup("sfg-001") + assert route.called + assert result == spatialfeaturegroup_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_spatialfeaturegroup_not_found(er_client): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeaturegroup/nonexistent") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.get_spatialfeaturegroup("nonexistent") + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_feature_group_deprecated_delegates_to_get_spatialfeaturegroup( + er_client, spatialfeaturegroup_response +): + """get_feature_group is deprecated and delegates to get_spatialfeaturegroup.""" + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeaturegroup/sfg-001") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeaturegroup_response}) + with pytest.warns(DeprecationWarning, match="get_feature_group.*get_spatialfeaturegroup"): + result = await er_client.get_feature_group("sfg-001") + assert route.called + assert result == spatialfeaturegroup_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_post_spatialfeaturegroup(er_client, spatialfeaturegroup_payload, spatialfeaturegroup_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.post("spatialfeaturegroup") + route.return_value = httpx.Response(httpx.codes.CREATED, json={"data": spatialfeaturegroup_response}) + result = await er_client.post_spatialfeaturegroup(spatialfeaturegroup_payload) + assert route.called + assert result == spatialfeaturegroup_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_post_spatialfeaturegroup_forbidden(er_client, spatialfeaturegroup_payload): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.post("spatialfeaturegroup") + route.return_value = httpx.Response(httpx.codes.FORBIDDEN, json={"status": {"code": 403}}) + with pytest.raises(ERClientPermissionDenied): + await er_client.post_spatialfeaturegroup(spatialfeaturegroup_payload) + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_patch_spatialfeaturegroup(er_client, spatialfeaturegroup_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.patch("spatialfeaturegroup/sfg-001") + updated = {**spatialfeaturegroup_response, "name": "Updated Name"} + route.return_value = httpx.Response(httpx.codes.OK, json={"data": updated}) + result = await er_client.patch_spatialfeaturegroup("sfg-001", {"name": "Updated Name"}) + assert route.called + assert result["name"] == "Updated Name" + await er_client.close() + + +@pytest.mark.asyncio +async def test_patch_spatialfeaturegroup_not_found(er_client): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.patch("spatialfeaturegroup/nonexistent") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.patch_spatialfeaturegroup("nonexistent", {"name": "x"}) + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_delete_spatialfeaturegroup(er_client): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.delete("spatialfeaturegroup/sfg-001/") + route.return_value = httpx.Response(httpx.codes.NO_CONTENT) + result = await er_client.delete_spatialfeaturegroup("sfg-001") + assert route.called + assert result is True # 204 No Content returns True (canonical _delete behavior) + await er_client.close() + + +@pytest.mark.asyncio +async def test_delete_spatialfeaturegroup_not_found(er_client): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.delete("spatialfeaturegroup/nonexistent/") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.delete_spatialfeaturegroup("nonexistent") + assert route.called + await er_client.close() + + +# -- Spatial Feature CRUD tests -- + +@pytest.mark.asyncio +async def test_get_spatialfeatures(er_client, spatialfeatures_list_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeature") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeatures_list_response}) + result = await er_client.get_spatialfeatures() + assert route.called + assert result == spatialfeatures_list_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_spatialfeature(er_client, spatialfeature_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeature/sf-001") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeature_response}) + result = await er_client.get_spatialfeature("sf-001") + assert route.called + assert result == spatialfeature_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_spatialfeature_not_found(er_client): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeature/nonexistent") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.get_spatialfeature("nonexistent") + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_post_spatialfeature(er_client, spatialfeature_payload, spatialfeature_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.post("spatialfeature") + route.return_value = httpx.Response(httpx.codes.CREATED, json={"data": spatialfeature_response}) + result = await er_client.post_spatialfeature(spatialfeature_payload) + assert route.called + assert result == spatialfeature_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_post_spatialfeature_forbidden(er_client, spatialfeature_payload): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.post("spatialfeature") + route.return_value = httpx.Response(httpx.codes.FORBIDDEN, json={"status": {"code": 403}}) + with pytest.raises(ERClientPermissionDenied): + await er_client.post_spatialfeature(spatialfeature_payload) + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_patch_spatialfeature(er_client, spatialfeature_response): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.patch("spatialfeature/sf-001") + updated = {**spatialfeature_response, "name": "Updated Park"} + route.return_value = httpx.Response(httpx.codes.OK, json={"data": updated}) + result = await er_client.patch_spatialfeature("sf-001", {"name": "Updated Park"}) + assert route.called + assert result["name"] == "Updated Park" + await er_client.close() + + +@pytest.mark.asyncio +async def test_patch_spatialfeature_not_found(er_client): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.patch("spatialfeature/nonexistent") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.patch_spatialfeature("nonexistent", {"name": "x"}) + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_delete_spatialfeature(er_client): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.delete("spatialfeature/sf-001/") + route.return_value = httpx.Response(httpx.codes.NO_CONTENT) + result = await er_client.delete_spatialfeature("sf-001") + assert route.called + assert result is True # 204 No Content returns True (canonical _delete behavior) + await er_client.close() + + +@pytest.mark.asyncio +async def test_delete_spatialfeature_not_found(er_client): + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: + route = respx_mock.delete("spatialfeature/nonexistent/") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.delete_spatialfeature("nonexistent") + assert route.called + await er_client.close() diff --git a/tests/sync_client/test_mapping_spatial.py b/tests/sync_client/test_mapping_spatial.py new file mode 100644 index 0000000..6afc199 --- /dev/null +++ b/tests/sync_client/test_mapping_spatial.py @@ -0,0 +1,278 @@ +import json +from unittest.mock import MagicMock + +import pytest + +from erclient import ERClientNotFound, ERClientPermissionDenied + + +def _mock_response(status_code=200, json_data=None): + """Helper to create a mock response object.""" + response = MagicMock() + response.ok = 200 <= status_code < 400 + response.status_code = status_code + response.text = json.dumps(json_data) if json_data else "" + response.json.return_value = json_data + response.url = "https://fake-site.erdomain.org/api/v1.0/test" + return response + + +# -- Read-only endpoint tests -- + + +def test_get_features(er_client): + expected = [{"type": "Feature", "properties": {"name": "Post A"}}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_features() + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_feature(er_client): + expected = {"type": "Feature", "properties": {"name": "Post A", "id": "fp-001"}} + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_feature("fp-001") + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_feature_not_found(er_client): + er_client._http_session.get = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.get_feature("nonexistent") + + +def test_get_featuresets(er_client): + expected = [{"id": "fs-001", "name": "Ranger Posts"}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_featuresets() + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_featureset(er_client): + expected = {"id": "fs-001", "name": "Ranger Posts"} + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_featureset("fs-001") + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_maps(er_client): + expected = [{"id": "map-001", "title": "Park Overview"}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_maps() + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_layers(er_client): + expected = [{"id": "layer-001", "title": "Roads"}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_layers() + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_layer(er_client): + expected = {"id": "layer-001", "title": "Roads"} + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_layer("layer-001") + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_featureclasses(er_client): + expected = [{"id": "fc-001", "name": "boundary"}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_featureclasses() + er_client._http_session.get.assert_called_once() + assert result == expected + + +# -- Spatial Feature Group CRUD tests -- + + +def test_get_spatialfeaturegroups(er_client): + expected = [{"id": "sfg-001", "name": "Protected Areas"}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_spatialfeaturegroups() + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_spatialfeaturegroup(er_client): + expected = {"id": "sfg-001", "name": "Protected Areas"} + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_spatialfeaturegroup("sfg-001") + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_spatialfeaturegroup_not_found(er_client): + er_client._http_session.get = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.get_spatialfeaturegroup("nonexistent") + + +def test_post_spatialfeaturegroup(er_client): + payload = {"name": "Protected Areas"} + created = {"data": {"id": "sfg-001", "name": "Protected Areas"}, "status": {"code": 201}} + er_client._http_session.post = MagicMock( + return_value=_mock_response(201, created) + ) + result = er_client.post_spatialfeaturegroup(payload) + er_client._http_session.post.assert_called_once() + assert result == created["data"] + + +def test_post_spatialfeaturegroup_forbidden(er_client): + er_client._http_session.post = MagicMock( + return_value=_mock_response(403, {"status": {"detail": "Forbidden"}}) + ) + with pytest.raises(ERClientPermissionDenied): + er_client.post_spatialfeaturegroup({"name": "Test"}) + + +def test_patch_spatialfeaturegroup(er_client): + updated = {"data": {"id": "sfg-001", "name": "Renamed"}, "status": {"code": 200}} + er_client._http_session.patch = MagicMock( + return_value=_mock_response(200, updated) + ) + result = er_client.patch_spatialfeaturegroup("sfg-001", {"name": "Renamed"}) + er_client._http_session.patch.assert_called_once() + assert result["name"] == "Renamed" + + +def test_patch_spatialfeaturegroup_not_found(er_client): + er_client._http_session.patch = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.patch_spatialfeaturegroup("nonexistent", {"name": "X"}) + + +def test_delete_spatialfeaturegroup(er_client): + er_client._http_session.delete = MagicMock( + return_value=_mock_response(204) + ) + result = er_client.delete_spatialfeaturegroup("sfg-001") + er_client._http_session.delete.assert_called_once() + assert result is True + + +def test_delete_spatialfeaturegroup_not_found(er_client): + er_client._http_session.delete = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.delete_spatialfeaturegroup("nonexistent") + + +# -- Spatial Feature CRUD tests -- + + +def test_get_spatialfeatures(er_client): + expected = [{"id": "sf-001", "name": "Nairobi NP"}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_spatialfeatures() + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_spatialfeature(er_client): + expected = {"id": "sf-001", "name": "Nairobi NP"} + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_spatialfeature("sf-001") + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_spatialfeature_not_found(er_client): + er_client._http_session.get = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.get_spatialfeature("nonexistent") + + +def test_post_spatialfeature(er_client): + payload = {"name": "Nairobi NP"} + created = {"data": {"id": "sf-001", "name": "Nairobi NP"}, "status": {"code": 201}} + er_client._http_session.post = MagicMock( + return_value=_mock_response(201, created) + ) + result = er_client.post_spatialfeature(payload) + er_client._http_session.post.assert_called_once() + assert result == created["data"] + + +def test_post_spatialfeature_forbidden(er_client): + er_client._http_session.post = MagicMock( + return_value=_mock_response(403, {"status": {"detail": "Forbidden"}}) + ) + with pytest.raises(ERClientPermissionDenied): + er_client.post_spatialfeature({"name": "Test"}) + + +def test_patch_spatialfeature(er_client): + updated = {"data": {"id": "sf-001", "name": "Updated Park"}, "status": {"code": 200}} + er_client._http_session.patch = MagicMock( + return_value=_mock_response(200, updated) + ) + result = er_client.patch_spatialfeature("sf-001", {"name": "Updated Park"}) + er_client._http_session.patch.assert_called_once() + assert result["name"] == "Updated Park" + + +def test_patch_spatialfeature_not_found(er_client): + er_client._http_session.patch = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.patch_spatialfeature("nonexistent", {"name": "X"}) + + +def test_delete_spatialfeature(er_client): + er_client._http_session.delete = MagicMock( + return_value=_mock_response(204) + ) + result = er_client.delete_spatialfeature("sf-001") + er_client._http_session.delete.assert_called_once() + assert result is True + + +def test_delete_spatialfeature_not_found(er_client): + er_client._http_session.delete = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.delete_spatialfeature("nonexistent")