test_rpc.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. from __future__ import annotations
  2. from typing import Any, cast
  3. from unittest import mock
  4. import pytest
  5. import responses
  6. from django.db import router
  7. from django.test import override_settings
  8. from sentry.models import OrganizationMapping
  9. from sentry.services.hybrid_cloud.actor import RpcActor
  10. from sentry.services.hybrid_cloud.auth import AuthService
  11. from sentry.services.hybrid_cloud.organization import (
  12. OrganizationService,
  13. RpcOrganizationMemberFlags,
  14. RpcUserOrganizationContext,
  15. )
  16. from sentry.services.hybrid_cloud.organization.serial import serialize_rpc_organization
  17. from sentry.services.hybrid_cloud.rpc import (
  18. RpcServiceSetupException,
  19. dispatch_remote_call,
  20. dispatch_to_local_service,
  21. )
  22. from sentry.services.hybrid_cloud.user import RpcUser
  23. from sentry.services.hybrid_cloud.user.serial import serialize_rpc_user
  24. from sentry.silo import SiloMode, unguarded_write
  25. from sentry.testutils.cases import TestCase
  26. from sentry.testutils.region import override_regions
  27. from sentry.testutils.silo import assume_test_silo_mode
  28. from sentry.types.region import Region, RegionCategory
  29. from sentry.utils import json
  30. _REGIONS = [
  31. Region("north_america", 1, "http://na.sentry.io", RegionCategory.MULTI_TENANT, "swordfish"),
  32. Region("europe", 2, "http://eu.sentry.io", RegionCategory.MULTI_TENANT, "courage"),
  33. ]
  34. class RpcServiceTest(TestCase):
  35. @mock.patch("sentry.services.hybrid_cloud.rpc.dispatch_remote_call")
  36. def test_remote_service(self, mock_dispatch_remote_call):
  37. target_region = _REGIONS[0]
  38. user = self.create_user()
  39. organization = self.create_organization()
  40. with unguarded_write(using=router.db_for_write(OrganizationMapping)):
  41. OrganizationMapping.objects.update_or_create(
  42. organization_id=organization.id,
  43. defaults={
  44. "slug": organization.slug,
  45. "name": organization.name,
  46. "region_name": target_region.name,
  47. },
  48. )
  49. serial_user = RpcUser(id=user.id)
  50. serial_org = serialize_rpc_organization(organization)
  51. service = OrganizationService.create_delegation()
  52. with override_regions(_REGIONS), override_settings(SILO_MODE=SiloMode.CONTROL):
  53. service.add_organization_member(
  54. organization_id=serial_org.id,
  55. default_org_role=serial_org.default_role,
  56. user=serial_user,
  57. flags=RpcOrganizationMemberFlags(),
  58. role=None,
  59. )
  60. assert mock_dispatch_remote_call.called
  61. (
  62. region,
  63. service_name,
  64. method_name,
  65. serial_arguments,
  66. ) = mock_dispatch_remote_call.call_args.args
  67. assert region == target_region
  68. assert service_name == OrganizationService.key
  69. assert method_name == "add_organization_member"
  70. assert serial_arguments.keys() == {
  71. "organization_id",
  72. "default_org_role",
  73. "user_id",
  74. "email",
  75. "flags",
  76. "role",
  77. "inviter_id",
  78. "invite_status",
  79. }
  80. assert serial_arguments["organization_id"] == organization.id
  81. @mock.patch("sentry.services.hybrid_cloud.in_test_environment")
  82. @mock.patch("sentry.services.hybrid_cloud.report_pydantic_type_validation_error")
  83. def test_models_tolerate_invalid_types(self, mock_report, mock_in_test_environment):
  84. # Check reporting of validation errors, rather than suppressing as in most tests
  85. mock_in_test_environment.return_value = False
  86. # Create an RpcModel instance whose fields don't obey type annotations and
  87. # ensure that it does not raise an exception.
  88. RpcActor(
  89. id="hey, this isn't an int",
  90. actor_id=None, # this one is okay
  91. actor_type=None, # should not be Optional
  92. )
  93. assert mock_report.call_count == 2
  94. field_names = {c.args[0].name for c in mock_report.call_args_list}
  95. model_classes = [c.args[3] for c in mock_report.call_args_list]
  96. assert field_names == {"id", "actor_type"}
  97. assert model_classes == [RpcActor] * 2
  98. def test_dispatch_to_local_service(self):
  99. user = self.create_user()
  100. organization = self.create_organization()
  101. serial_org = serialize_rpc_organization(organization)
  102. serial_arguments = dict(
  103. organization_id=serial_org.id,
  104. default_org_role=serial_org.default_role,
  105. user_id=user.id,
  106. flags=RpcOrganizationMemberFlags().dict(),
  107. role=None,
  108. )
  109. with assume_test_silo_mode(SiloMode.REGION):
  110. service = OrganizationService.create_delegation()
  111. dispatch_to_local_service(service.key, "add_organization_member", serial_arguments)
  112. def test_dispatch_to_local_service_list_result(self):
  113. organization = self.create_organization()
  114. args = {"organization_ids": [organization.id]}
  115. with assume_test_silo_mode(SiloMode.CONTROL):
  116. service = AuthService.create_delegation()
  117. response = dispatch_to_local_service(service.key, "get_org_auth_config", args)
  118. result = response["value"]
  119. assert len(result) == 1
  120. assert result[0]["organization_id"] == organization.id
  121. control_address = "https://control.example.com"
  122. shared_secret = ["a-long-token-you-could-not-guess"]
  123. class DispatchRemoteCallTest(TestCase):
  124. @override_settings(
  125. SILO_MODE=SiloMode.CONTROL,
  126. RPC_SHARED_SECRET=[],
  127. SENTRY_CONTROL_ADDRESS="",
  128. )
  129. def test_while_not_allowed(self):
  130. with pytest.raises(RpcServiceSetupException):
  131. dispatch_remote_call(None, "user", "get_user", {"user_id": 0})
  132. @staticmethod
  133. def _set_up_mock_response(service_name: str, response_value: Any, address: str | None = None):
  134. address = address or control_address
  135. responses.add(
  136. responses.POST,
  137. f"{address}/api/0/internal/rpc/{service_name}/",
  138. content_type="json",
  139. body=json.dumps({"meta": {}, "value": response_value}),
  140. )
  141. @responses.activate
  142. def test_region_to_control_happy_path(self):
  143. org = self.create_organization()
  144. with override_settings(
  145. RPC_SHARED_SECRET=shared_secret, SENTRY_CONTROL_ADDRESS=control_address
  146. ):
  147. response_value = RpcUserOrganizationContext(
  148. organization=serialize_rpc_organization(org)
  149. )
  150. self._set_up_mock_response("organization/get_organization_by_id", response_value.dict())
  151. result = dispatch_remote_call(
  152. None, "organization", "get_organization_by_id", {"id": org.id}
  153. )
  154. assert result == response_value
  155. @responses.activate
  156. @override_settings(
  157. SILO_MODE=SiloMode.REGION,
  158. RPC_SHARED_SECRET=shared_secret,
  159. SENTRY_CONTROL_ADDRESS=control_address,
  160. )
  161. def test_region_to_control_null_result(self):
  162. self._set_up_mock_response("organization/get_organization_by_id", None)
  163. result = dispatch_remote_call(None, "organization", "get_organization_by_id", {"id": 0})
  164. assert result is None
  165. @responses.activate
  166. @override_regions(_REGIONS)
  167. @override_settings(
  168. SILO_MODE=SiloMode.CONTROL,
  169. RPC_SHARED_SECRET=shared_secret,
  170. SENTRY_CONTROL_ADDRESS=control_address,
  171. )
  172. def test_control_to_region_happy_path(self):
  173. user = self.create_user()
  174. serial = serialize_rpc_user(user)
  175. self._set_up_mock_response("user/get_user", serial.dict(), address="http://na.sentry.io")
  176. result = dispatch_remote_call(_REGIONS[0], "user", "get_user", {"id": 0})
  177. assert result == serial
  178. @responses.activate
  179. @override_regions(_REGIONS)
  180. @override_settings(
  181. SILO_MODE=SiloMode.CONTROL,
  182. RPC_SHARED_SECRET=shared_secret,
  183. SENTRY_CONTROL_ADDRESS=control_address,
  184. )
  185. def test_region_to_control_with_list_result(self):
  186. users = [self.create_user() for _ in range(3)]
  187. serial = [serialize_rpc_user(user) for user in users]
  188. self._set_up_mock_response("user/get_many", [m.dict() for m in serial])
  189. result = dispatch_remote_call(None, "user", "get_many", {"filter": {}})
  190. assert result == serial
  191. @responses.activate
  192. @override_regions(_REGIONS)
  193. @override_settings(SILO_MODE=SiloMode.CONTROL, DEV_HYBRID_CLOUD_RPC_SENDER={"is_allowed": True})
  194. def test_early_halt_from_null_region_resolution(self):
  195. with override_settings(SILO_MODE=SiloMode.CONTROL):
  196. org_service_delgn = cast(
  197. OrganizationService, OrganizationService.create_delegation(use_test_client=False)
  198. )
  199. result = org_service_delgn.get_org_by_slug(slug="this_is_not_a_valid_slug")
  200. assert result is None