From 4f4f1e3acad13b36258ecfc60f8998df04cbc4f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:30:02 +0000 Subject: [PATCH 1/4] Initial plan From cd5aca517e2a74b128c8267ca4fa26f16ea4c60d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:44:31 +0000 Subject: [PATCH 2/4] Add comprehensive test coverage for ring_configuration and personal endpoints Co-authored-by: godely <3101049+godely@users.noreply.github.com> --- tests/test_client.py | 302 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index c3df0ca..75f4641 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -44,6 +44,12 @@ from oura_api_client.models.vo2_max import ( Vo2MaxResponse, Vo2MaxModel ) # Added Vo2Max models +from oura_api_client.models.ring_configuration import ( + RingConfigurationResponse, RingConfigurationModel +) # Added RingConfiguration models +from oura_api_client.models.personal import ( + PersonalInfo +) # Added Personal models import requests from oura_api_client.exceptions import ( @@ -2670,3 +2676,299 @@ def test_get_vo2_max_document_not_found_404(self, mock_get): with self.assertRaises(OuraNotFoundError): self.client.vo2_max.get_vo2_max_document(document_id) + + +class TestRingConfiguration(unittest.TestCase): + def setUp(self): + self.client = OuraClient(access_token="test_token") + self.base_url = self.client.BASE_URL + + @patch("requests.get") + def test_get_ring_configuration_documents_no_params(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + response = self.client.ring_configuration.get_ring_configuration_documents() + self.assertIsInstance(response, RingConfigurationResponse) + mock_get.assert_called_once_with( + f"{self.base_url}/usercollection/ring_configuration", + headers=self.client.headers, + params=None, + timeout=30.0, + ) + + @patch("requests.get") + def test_get_ring_configuration_documents_start_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.ring_configuration.get_ring_configuration_documents( + start_date="2024-03-01" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/usercollection/ring_configuration", + headers=self.client.headers, + params={"start_date": "2024-03-01"}, + timeout=30.0, + ) + + @patch("requests.get") + def test_get_ring_configuration_documents_end_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.ring_configuration.get_ring_configuration_documents( + end_date="2024-02-28" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/usercollection/ring_configuration", + headers=self.client.headers, + params={"end_date": "2024-02-28"}, + timeout=30.0, + ) + + @patch("requests.get") + def test_get_ring_configuration_documents_start_and_end_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.ring_configuration.get_ring_configuration_documents( + start_date="2024-02-01", end_date="2024-02-28" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/usercollection/ring_configuration", + headers=self.client.headers, + params={"start_date": "2024-02-01", "end_date": "2024-02-28"}, + timeout=30.0, + ) + + @patch("requests.get") + def test_get_ring_configuration_documents_next_token(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.ring_configuration.get_ring_configuration_documents( + next_token="next_token_string" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/usercollection/ring_configuration", + headers=self.client.headers, + params={"next_token": "next_token_string"}, + timeout=30.0, + ) + + @patch("requests.get") + def test_get_ring_configuration_documents_success(self, mock_get): + mock_response_data = { + "data": [{ + "id": "ring_config_1", + "color": "silver", + "design": "heritage", + "firmware_version": "2.1.0", + "hardware_type": "gen3", + "set_up_at": "2024-01-15T10:00:00+00:00", + "size": 8 + }], + "next_token": "ring_next_token" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + response = self.client.ring_configuration.get_ring_configuration_documents( + start_date="2024-01-15" + ) + self.assertIsInstance(response, RingConfigurationResponse) + self.assertEqual(len(response.data), 1) + model_item = response.data[0] + self.assertIsInstance(model_item, RingConfigurationModel) + self.assertEqual(model_item.id, "ring_config_1") + self.assertEqual(model_item.color, "silver") + self.assertEqual(model_item.design, "heritage") + self.assertEqual(model_item.firmware_version, "2.1.0") + self.assertEqual(model_item.hardware_type, "gen3") + self.assertEqual(model_item.size, 8) + self.assertEqual(response.next_token, "ring_next_token") + mock_get.assert_called_with( + f"{self.base_url}/usercollection/ring_configuration", + headers=self.client.headers, + params={"start_date": "2024-01-15"}, + timeout=30.0, + ) + + @patch("requests.get") + def test_get_ring_configuration_documents_api_error_400(self, mock_get): + mock_response = MagicMock() + mock_response.ok = False + mock_response.status_code = 400 + mock_response.reason = "Bad Request" + mock_response.json.return_value = {"error": "400 Client Error"} + mock_get.return_value = mock_response + with self.assertRaises(OuraClientError): + self.client.ring_configuration.get_ring_configuration_documents() + + @patch("requests.get") + def test_get_ring_configuration_document_success(self, mock_get): + document_id = "ring_config_test_id" + mock_response_json = { + "id": document_id, + "color": "black", + "design": "horizon", + "firmware_version": "2.2.1", + "hardware_type": "gen3", + "set_up_at": "2024-02-01T08:30:00+00:00", + "size": 9 + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + response = self.client.ring_configuration.get_ring_configuration_document( + document_id=document_id + ) + self.assertIsInstance(response, RingConfigurationModel) + self.assertEqual(response.id, document_id) + self.assertEqual(response.color, "black") + self.assertEqual(response.design, "horizon") + self.assertEqual(response.firmware_version, "2.2.1") + self.assertEqual(response.hardware_type, "gen3") + self.assertEqual(response.size, 9) + + mock_get.assert_called_once_with( + f"{self.base_url}/usercollection/ring_configuration/{document_id}", + headers=self.client.headers, + params=None, + timeout=30.0, + ) + + @patch("requests.get") + def test_get_ring_configuration_document_not_found_404(self, mock_get): + document_id = "non_existent_ring_config_id" + mock_response = MagicMock() + mock_response.ok = False + mock_response.status_code = 404 + mock_response.reason = "Not Found" + mock_response.json.return_value = {"error": "404 Client Error: Not Found"} + mock_get.return_value = mock_response + + with self.assertRaises(OuraNotFoundError): + self.client.ring_configuration.get_ring_configuration_document(document_id) + + +class TestPersonal(unittest.TestCase): + def setUp(self): + self.client = OuraClient(access_token="test_token") + self.base_url = self.client.BASE_URL + + @patch("requests.get") + def test_get_personal_info_success(self, mock_get): + mock_response_data = { + "id": "user_123", + "email": "test@example.com", + "age": 30, + "weight": 70.5, + "height": 175.0, + "biological_sex": "male", + "birth_date": "1993-05-15" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + response = self.client.personal.get_personal_info() + self.assertIsInstance(response, PersonalInfo) + self.assertEqual(response.id, "user_123") + self.assertEqual(response.email, "test@example.com") + self.assertEqual(response.age, 30) + self.assertEqual(response.weight, 70.5) + self.assertEqual(response.height, 175.0) + self.assertEqual(response.biological_sex, "male") + self.assertEqual(response.birth_date, date(1993, 5, 15)) + + mock_get.assert_called_once_with( + f"{self.base_url}/usercollection/personal_info", + headers=self.client.headers, + params=None, + timeout=30.0, + ) + + @patch("requests.get") + def test_get_personal_info_raw_response(self, mock_get): + mock_response_data = { + "id": "user_123", + "email": "test@example.com", + "age": 30, + "weight": 70.5, + "height": 175.0, + "biological_sex": "male", + "birth_date": "1993-05-15" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + response = self.client.personal.get_personal_info(return_model=False) + self.assertIsInstance(response, dict) + self.assertEqual(response["id"], "user_123") + self.assertEqual(response["email"], "test@example.com") + self.assertEqual(response["age"], 30) + + mock_get.assert_called_once_with( + f"{self.base_url}/usercollection/personal_info", + headers=self.client.headers, + params=None, + timeout=30.0, + ) + + @patch("requests.get") + def test_get_personal_info_api_error_401(self, mock_get): + mock_response = MagicMock() + mock_response.ok = False + mock_response.status_code = 401 + mock_response.reason = "Unauthorized" + mock_response.json.return_value = {"error": "401 Client Error: Unauthorized"} + mock_get.return_value = mock_response + + with self.assertRaises(Exception): # Specific exception depends on error handling implementation + self.client.personal.get_personal_info() + + @patch("requests.get") + def test_get_personal_info_minimal_data(self, mock_get): + mock_response_data = { + "id": "user_456", + "email": "minimal@example.com", + "age": 25 + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + response = self.client.personal.get_personal_info() + self.assertIsInstance(response, PersonalInfo) + self.assertEqual(response.id, "user_456") + self.assertEqual(response.email, "minimal@example.com") + self.assertEqual(response.age, 25) + self.assertIsNone(response.weight) + self.assertIsNone(response.height) + self.assertIsNone(response.biological_sex) + self.assertIsNone(response.birth_date) From dc618d796b95b7968bd1dd1d8ff94d20002ebbb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:12:42 +0000 Subject: [PATCH 3/4] Resolve merge conflicts in test_client.py by adding ring_configuration assertion Co-authored-by: godely <3101049+godely@users.noreply.github.com> --- tests/test_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 75f4641..06f239a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -92,6 +92,8 @@ def test_initialization(self): self.assertIsNotNone(self.client.daily_cardiovascular_age) # Added vo2_max self.assertIsNotNone(self.client.vo2_max) + # Added ring_configuration + self.assertIsNotNone(self.client.ring_configuration) @patch("requests.get") def test_get_heart_rate(self, mock_get): From 02e797292a032ce171bd8943e548a33fcc62f0a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:29:13 +0000 Subject: [PATCH 4/4] Resolve merge conflicts by adding missing TestWebhook and TestHttpMethods classes Co-authored-by: godely <3101049+godely@users.noreply.github.com> --- tests/test_client.py | 88 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 06f239a..810c4ad 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2974,3 +2974,91 @@ def test_get_personal_info_minimal_data(self, mock_get): self.assertIsNone(response.height) self.assertIsNone(response.biological_sex) self.assertIsNone(response.birth_date) + + +class TestWebhook(unittest.TestCase): + def test_webhook_requires_credentials(self): + """Test that webhook operations require client_id and client_secret.""" + client_without_creds = OuraClient(access_token="test_token") + + with self.assertRaises(ValueError) as context: + client_without_creds.webhook.list_webhook_subscriptions() + + self.assertIn("client_id and client_secret must be set", str(context.exception)) + + # The method should not have made any HTTP requests + # This is verified by the fact that no mock_get.assert_not_called() is needed + + +class TestHttpMethods(unittest.TestCase): + """Test cases for HTTP method support in _make_request.""" + + def setUp(self): + """Set up client.""" + from oura_api_client.utils.retry import RetryConfig + # Disable retry to avoid network calls during method validation + retry_config = RetryConfig(enabled=False) + self.client = OuraClient(access_token="test_token", retry_config=retry_config) + + @patch("requests.post") + def test_post_method_with_json_data(self, mock_post): + """Test POST method with JSON data.""" + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"success": True} + mock_post.return_value = mock_response + + # Test that unsupported methods raise ValueError + with self.assertRaises(ValueError) as context: + self.client._make_request("/test", method="POST") + + self.assertIn("POST is not supported", str(context.exception)) + + @patch("requests.put") + def test_put_method_without_json_data(self, mock_put): + """Test PUT method without JSON data.""" + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"updated": True} + mock_put.return_value = mock_response + + # Test that unsupported methods raise ValueError + with self.assertRaises(ValueError) as context: + self.client._make_request("/test", method="PUT") + + self.assertIn("PUT is not supported", str(context.exception)) + + @patch("requests.delete") + def test_delete_method_empty_response(self, mock_delete): + """Test DELETE method with empty response.""" + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.content = b"" + mock_delete.return_value = mock_response + + # Test that unsupported methods raise ValueError + with self.assertRaises(ValueError) as context: + self.client._make_request("/test", method="DELETE") + + self.assertIn("DELETE is not supported", str(context.exception)) + + @patch("requests.patch") + def test_patch_method_with_headers(self, mock_patch): + """Test PATCH method with custom headers.""" + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"patched": True} + mock_patch.return_value = mock_response + + # Test that unsupported methods raise ValueError + with self.assertRaises(ValueError) as context: + self.client._make_request("/test", method="PATCH") + + self.assertIn("PATCH is not supported", str(context.exception)) + + def test_unsupported_method(self): + """Test that unsupported HTTP methods raise ValueError.""" + with self.assertRaises(ValueError) as context: + self.client._make_request("/test", method="TRACE") + + self.assertIn("TRACE is not supported", str(context.exception))