|
7 | 7 | from urllib.parse import parse_qs, parse_qsl, urlparse |
8 | 8 |
|
9 | 9 | import orjson |
| 10 | +import pytest |
10 | 11 | import responses |
11 | 12 | from django.contrib.messages.storage.fallback import FallbackStorage |
12 | 13 | from django.contrib.sessions.backends.base import SessionBase |
|
15 | 16 | from requests.exceptions import SSLError |
16 | 17 |
|
17 | 18 | import sentry.identity |
| 19 | +from sentry.auth.exceptions import IdentityNotValid |
18 | 20 | from sentry.identity.datadog.provider import ( |
| 21 | + DatadogIdentityProvider, |
19 | 22 | DatadogOAuth2CallbackView, |
20 | 23 | DatadogOAuth2DCRView, |
21 | 24 | DatadogOAuth2LoginView, |
@@ -381,3 +384,106 @@ def test_pipeline_views(self, mock_record: MagicMock) -> None: |
381 | 384 | f"Basic {base64.b64encode(b'dcr-client-id:dcr-client-secret').decode()}" |
382 | 385 | ) |
383 | 386 | assert exchange_token_request.headers["Authorization"] == expected_auth_header |
| 387 | + |
| 388 | + |
| 389 | +@control_silo_test |
| 390 | +class DatadogIdentityProviderBuildIdentityTest(TestCase): |
| 391 | + def setUp(self) -> None: |
| 392 | + super().setUp() |
| 393 | + self.provider = DatadogIdentityProvider() |
| 394 | + self.provider.config = {"site": "datadoghq.com"} |
| 395 | + |
| 396 | + @patch("sentry.identity.datadog.provider.get_user_info") |
| 397 | + def test_build_identity(self, mock_get_user_info: MagicMock) -> None: |
| 398 | + mock_get_user_info.return_value = { |
| 399 | + "id": "dd-user-123", |
| 400 | + "attributes": {"email": "user@example.com", "name": "Test User"}, |
| 401 | + } |
| 402 | + |
| 403 | + result = self.provider.build_identity( |
| 404 | + { |
| 405 | + "data": { |
| 406 | + "access_token": "token-abc", |
| 407 | + "refresh_token": "refresh-xyz", |
| 408 | + "expires_in": 3600, |
| 409 | + "token_type": "Bearer", |
| 410 | + "scope": "apm_read", |
| 411 | + }, |
| 412 | + "dcr_client_id": "dcr-client-id", |
| 413 | + "dcr_client_secret": "dcr-client-secret", |
| 414 | + } |
| 415 | + ) |
| 416 | + |
| 417 | + assert result["id"] == "dd-user-123" |
| 418 | + assert result["email"] == "user@example.com" |
| 419 | + assert result["name"] == "Test User" |
| 420 | + assert result["type"] == "datadog" |
| 421 | + assert result["data"]["access_token"] == "token-abc" |
| 422 | + assert result["data"]["refresh_token"] == "refresh-xyz" |
| 423 | + assert "expires" in result["data"] |
| 424 | + assert result["data"]["token_type"] == "Bearer" |
| 425 | + assert result["data"]["scope"] == "apm_read" |
| 426 | + assert result["data"]["client_id"] == "dcr-client-id" |
| 427 | + assert result["data"]["client_secret"] == "dcr-client-secret" |
| 428 | + mock_get_user_info.assert_called_once_with("token-abc", "datadoghq.com") |
| 429 | + |
| 430 | + @patch("sentry.identity.datadog.provider.get_user_info") |
| 431 | + def test_build_identity_missing_access_token(self, mock_get_user_info: MagicMock) -> None: |
| 432 | + with pytest.raises(ValueError, match="did not return an access_token"): |
| 433 | + self.provider.build_identity({"data": {}}) |
| 434 | + mock_get_user_info.assert_not_called() |
| 435 | + |
| 436 | + @patch("sentry.identity.datadog.provider.get_user_info") |
| 437 | + def test_build_identity_missing_user_attributes(self, mock_get_user_info: MagicMock) -> None: |
| 438 | + mock_get_user_info.return_value = {"id": "dd-user-456", "attributes": {}} |
| 439 | + |
| 440 | + result = self.provider.build_identity({"data": {"access_token": "token"}}) |
| 441 | + |
| 442 | + assert result["id"] == "dd-user-456" |
| 443 | + assert result["email"] is None |
| 444 | + assert result["name"] is None |
| 445 | + |
| 446 | + |
| 447 | +@control_silo_test |
| 448 | +class DatadogIdentityProviderRefreshTest(TestCase): |
| 449 | + def setUp(self) -> None: |
| 450 | + super().setUp() |
| 451 | + self.provider = DatadogIdentityProvider() |
| 452 | + self.provider.config = {"site": "datadoghq.com"} |
| 453 | + |
| 454 | + @responses.activate |
| 455 | + def test_get_refresh_token_success(self) -> None: |
| 456 | + responses.add(responses.POST, TOKEN_URL, json={"access_token": "new-token"}) |
| 457 | + |
| 458 | + identity = MagicMock() |
| 459 | + identity.data = {"client_id": "dcr-client", "client_secret": "dcr-secret"} |
| 460 | + |
| 461 | + result = self.provider.get_refresh_token("refresh-token", TOKEN_URL, identity) |
| 462 | + |
| 463 | + assert result.status_code == 200 |
| 464 | + |
| 465 | + auth_header = responses.calls[0].request.headers["Authorization"] |
| 466 | + assert auth_header == f"Basic {base64.b64encode(b'dcr-client:dcr-secret').decode()}" |
| 467 | + |
| 468 | + data = dict(parse_qsl(responses.calls[0].request.body)) |
| 469 | + assert data["grant_type"] == "refresh_token" |
| 470 | + assert data["refresh_token"] == "refresh-token" |
| 471 | + assert "client_id" not in data |
| 472 | + assert "client_secret" not in data |
| 473 | + |
| 474 | + def test_get_refresh_token_missing_dcr_credentials(self) -> None: |
| 475 | + identity = MagicMock() |
| 476 | + identity.data = {} |
| 477 | + |
| 478 | + with pytest.raises(IdentityNotValid, match="Missing DCR credentials"): |
| 479 | + self.provider.get_refresh_token("refresh-token", TOKEN_URL, identity) |
| 480 | + |
| 481 | + @responses.activate |
| 482 | + def test_get_refresh_token_unauthorized(self) -> None: |
| 483 | + responses.add(responses.POST, TOKEN_URL, json={"error": "invalid_grant"}, status=401) |
| 484 | + |
| 485 | + identity = MagicMock() |
| 486 | + identity.data = {"client_id": "dcr-client-id", "client_secret": "dcr-client-secret"} |
| 487 | + |
| 488 | + with pytest.raises(IdentityNotValid): |
| 489 | + self.provider.get_refresh_token("refresh-token", TOKEN_URL, identity) |
0 commit comments