-
Notifications
You must be signed in to change notification settings - Fork 14
Add unit test coverage and CI coverage reporting #158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
bd02348
Add test coverage and JUnit XML reporting to CI pipelines
a9f0b7f
Add comprehensive unit tests for file upload (34 tests)
d2c5cc8
Add unit test cases for uncovered areas
29c9a5e
Add unit tests for _ODataClient internal methods (148 new tests)
f36fbd8
Merge from latest main
ea3aaaf
Fix 5 broken tests and add docstrings to TestAttributePayloadDtypes
c208e5d
test: review fixes and assertion strengthening
9361629
Add unit test coverage enforcement and CI reporting
ff4ef9a
Merge main: keep new test classes and response variable rename
44b8928
Apply black formatting
400ca95
Improve _batch.py coverage: add tests for dispatch, execute, and edge…
df693c1
Strengthen batch test assertions and fix duplicate TestContinueOnError
68e9804
Rename r1/r2 variables to req1/req2 for clarity
6187c63
Add UseDotNet prerequisite for PublishCodeCoverageResults@2
caba604
Improve test_odata_internal: rename r vars, add multi-record tests
3828c74
Add pathToSources to PublishCodeCoverageResults@2 to fix coverage tab…
da288fd
Move coverage config to pyproject.toml and simplify CI pytest command
792b870
Merge origin/main, resolve conflicts, add memo/multiline tests
ff26a98
Align test names with main
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
abelmilash-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT license. | ||
|
|
||
| import unittest | ||
| from unittest.mock import MagicMock | ||
|
|
||
| from azure.core.credentials import TokenCredential | ||
|
|
||
| from PowerPlatform.Dataverse.core._auth import _AuthManager, _TokenPair | ||
|
|
||
|
|
||
| class TestAuthManager(unittest.TestCase): | ||
| """Tests for _AuthManager credential validation and token acquisition.""" | ||
|
|
||
| def test_non_token_credential_raises(self): | ||
| """_AuthManager raises TypeError when credential does not implement TokenCredential.""" | ||
| with self.assertRaises(TypeError) as ctx: | ||
| _AuthManager("not-a-credential") | ||
| self.assertEqual( | ||
| str(ctx.exception), | ||
| "credential must implement azure.core.credentials.TokenCredential.", | ||
| ) | ||
|
|
||
| def test_acquire_token_returns_token_pair(self): | ||
| """_acquire_token calls get_token and returns a _TokenPair with scope and token.""" | ||
| mock_credential = MagicMock(spec=TokenCredential) | ||
| mock_credential.get_token.return_value = MagicMock(token="my-access-token") | ||
|
|
||
| manager = _AuthManager(mock_credential) | ||
| result = manager._acquire_token("https://org.crm.dynamics.com/.default") | ||
|
|
||
| mock_credential.get_token.assert_called_once_with("https://org.crm.dynamics.com/.default") | ||
| self.assertIsInstance(result, _TokenPair) | ||
| self.assertEqual(result.resource, "https://org.crm.dynamics.com/.default") | ||
| self.assertEqual(result.access_token, "my-access-token") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT license. | ||
|
|
||
| import unittest | ||
| from unittest.mock import MagicMock, patch, call | ||
|
|
||
| import requests | ||
|
|
||
| from PowerPlatform.Dataverse.core._http import _HttpClient | ||
|
|
||
|
|
||
| class TestHttpClientTimeout(unittest.TestCase): | ||
| """Tests for automatic timeout selection in _HttpClient._request.""" | ||
|
|
||
| def _make_response(self, status=200): | ||
| resp = MagicMock(spec=requests.Response) | ||
| resp.status_code = status | ||
| return resp | ||
|
|
||
| def test_get_uses_10s_default_timeout(self): | ||
| """GET requests use 10s default when no timeout is specified.""" | ||
| client = _HttpClient(retries=1) | ||
| with patch("requests.request", return_value=self._make_response()) as mock_req: | ||
| client._request("get", "https://example.com/data") | ||
| _, kwargs = mock_req.call_args | ||
| self.assertEqual(kwargs["timeout"], 10) | ||
|
|
||
| def test_post_uses_120s_default_timeout(self): | ||
| """POST requests use 120s default when no timeout is specified.""" | ||
| client = _HttpClient(retries=1) | ||
| with patch("requests.request", return_value=self._make_response()) as mock_req: | ||
| client._request("post", "https://example.com/data") | ||
| _, kwargs = mock_req.call_args | ||
| self.assertEqual(kwargs["timeout"], 120) | ||
|
|
||
| def test_delete_uses_120s_default_timeout(self): | ||
| """DELETE requests use 120s default when no timeout is specified.""" | ||
| client = _HttpClient(retries=1) | ||
| with patch("requests.request", return_value=self._make_response()) as mock_req: | ||
| client._request("delete", "https://example.com/data") | ||
| _, kwargs = mock_req.call_args | ||
| self.assertEqual(kwargs["timeout"], 120) | ||
|
|
||
| def test_default_timeout_overrides_per_method_default(self): | ||
| """Explicit default_timeout on the client overrides per-method defaults.""" | ||
| client = _HttpClient(retries=1, timeout=30.0) | ||
| with patch("requests.request", return_value=self._make_response()) as mock_req: | ||
| client._request("get", "https://example.com/data") | ||
| _, kwargs = mock_req.call_args | ||
| self.assertEqual(kwargs["timeout"], 30.0) | ||
|
|
||
| def test_explicit_timeout_kwarg_takes_precedence(self): | ||
| """If timeout is already in kwargs it is passed through unchanged.""" | ||
| client = _HttpClient(retries=1, timeout=30.0) | ||
| with patch("requests.request", return_value=self._make_response()) as mock_req: | ||
| client._request("get", "https://example.com/data", timeout=5) | ||
| _, kwargs = mock_req.call_args | ||
| self.assertEqual(kwargs["timeout"], 5) | ||
|
|
||
|
|
||
| class TestHttpClientRequester(unittest.TestCase): | ||
| """Tests for session vs direct requests.request routing.""" | ||
|
|
||
| def _make_response(self): | ||
| resp = MagicMock(spec=requests.Response) | ||
| resp.status_code = 200 | ||
| return resp | ||
|
|
||
| def test_uses_direct_request_without_session(self): | ||
| """Without a session, _request uses requests.request directly.""" | ||
| client = _HttpClient(retries=1) | ||
| with patch("requests.request", return_value=self._make_response()) as mock_req: | ||
| client._request("get", "https://example.com/data") | ||
| mock_req.assert_called_once() | ||
|
|
||
| def test_uses_session_request_when_session_provided(self): | ||
| """With a session, _request uses session.request instead of requests.request.""" | ||
| mock_session = MagicMock(spec=requests.Session) | ||
| mock_session.request.return_value = self._make_response() | ||
| client = _HttpClient(retries=1, session=mock_session) | ||
| with patch("requests.request") as mock_req: | ||
| client._request("get", "https://example.com/data") | ||
| mock_session.request.assert_called_once() | ||
| mock_req.assert_not_called() | ||
|
|
||
|
|
||
| class TestHttpClientRetry(unittest.TestCase): | ||
| """Tests for retry behavior on RequestException.""" | ||
|
|
||
| def test_retries_on_request_exception_and_succeeds(self): | ||
| """Retries after a RequestException and returns response on second attempt.""" | ||
| resp = MagicMock(spec=requests.Response) | ||
| resp.status_code = 200 | ||
| client = _HttpClient(retries=2, backoff=0) | ||
| with patch("requests.request", side_effect=[requests.exceptions.ConnectionError(), resp]) as mock_req: | ||
| with patch("time.sleep"): | ||
| result = client._request("get", "https://example.com/data") | ||
| self.assertEqual(mock_req.call_count, 2) | ||
| self.assertIs(result, resp) | ||
|
|
||
| def test_raises_after_all_retries_exhausted(self): | ||
| """Raises RequestException after all retry attempts fail.""" | ||
| client = _HttpClient(retries=3, backoff=0) | ||
| with patch("requests.request", side_effect=requests.exceptions.ConnectionError("timeout")): | ||
| with patch("time.sleep"): | ||
| with self.assertRaises(requests.exceptions.RequestException): | ||
| client._request("get", "https://example.com/data") | ||
|
|
||
| def test_backoff_delay_between_retries(self): | ||
| """Sleeps with exponential backoff between retry attempts.""" | ||
| resp = MagicMock(spec=requests.Response) | ||
| resp.status_code = 200 | ||
| client = _HttpClient(retries=3, backoff=1.0) | ||
| side_effects = [ | ||
| requests.exceptions.ConnectionError(), | ||
| requests.exceptions.ConnectionError(), | ||
| resp, | ||
| ] | ||
| with patch("requests.request", side_effect=side_effects): | ||
| with patch("time.sleep") as mock_sleep: | ||
| client._request("get", "https://example.com/data") | ||
| # First retry: delay = 1.0 * 2^0 = 1.0, second retry: 1.0 * 2^1 = 2.0 | ||
| mock_sleep.assert_has_calls([call(1.0), call(2.0)]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.