diff --git a/src/sentry/api/endpoints/monitor_checkin_attachment.py b/src/sentry/api/endpoints/organization_monitor_checkin_attachment.py similarity index 96% rename from src/sentry/api/endpoints/monitor_checkin_attachment.py rename to src/sentry/api/endpoints/organization_monitor_checkin_attachment.py index 049517e3e5d3fe..c86aaae9d4ccf2 100644 --- a/src/sentry/api/endpoints/monitor_checkin_attachment.py +++ b/src/sentry/api/endpoints/organization_monitor_checkin_attachment.py @@ -14,7 +14,7 @@ @region_silo_endpoint -class MonitorCheckInAttachmentEndpoint(MonitorCheckInEndpoint): +class OrganizationMonitorCheckInAttachmentEndpoint(MonitorCheckInEndpoint): # TODO(davidenwang): Add documentation after uploading feature is complete private = True authentication_classes = MonitorCheckInEndpoint.authentication_classes + (DSNAuthentication,) diff --git a/src/sentry/api/endpoints/monitor_details.py b/src/sentry/api/endpoints/organization_monitor_details.py similarity index 98% rename from src/sentry/api/endpoints/monitor_details.py rename to src/sentry/api/endpoints/organization_monitor_details.py index f24cd9b142ca79..eed97f16ff1ea7 100644 --- a/src/sentry/api/endpoints/monitor_details.py +++ b/src/sentry/api/endpoints/organization_monitor_details.py @@ -26,7 +26,7 @@ @region_silo_endpoint @extend_schema(tags=["Crons"]) -class MonitorDetailsEndpoint(MonitorEndpoint): +class OrganizationMonitorDetailsEndpoint(MonitorEndpoint): public = {"GET", "PUT", "DELETE"} @extend_schema( diff --git a/src/sentry/api/endpoints/monitor_stats.py b/src/sentry/api/endpoints/organization_monitor_stats.py similarity index 97% rename from src/sentry/api/endpoints/monitor_stats.py rename to src/sentry/api/endpoints/organization_monitor_stats.py index 28eb8f68c6ab3d..9fe6168033db41 100644 --- a/src/sentry/api/endpoints/monitor_stats.py +++ b/src/sentry/api/endpoints/organization_monitor_stats.py @@ -10,7 +10,7 @@ @region_silo_endpoint -class MonitorStatsEndpoint(MonitorEndpoint, StatsMixin): +class OrganizationMonitorStatsEndpoint(MonitorEndpoint, StatsMixin): # TODO(dcramer): probably convert to tsdb def get( self, request: Request, project, monitor, organization_slug: str | None = None diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index 02ae20ce21821d..3505fb35c6f4b1 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -190,11 +190,8 @@ InternalWarningsEndpoint, ) from .endpoints.issue_occurrence import IssueOccurrenceEndpoint -from .endpoints.monitor_checkin_attachment import MonitorCheckInAttachmentEndpoint from .endpoints.monitor_checkin_details import MonitorCheckInDetailsEndpoint from .endpoints.monitor_checkins import MonitorCheckInsEndpoint -from .endpoints.monitor_details import MonitorDetailsEndpoint -from .endpoints.monitor_stats import MonitorStatsEndpoint from .endpoints.organization_access_request_details import OrganizationAccessRequestDetailsEndpoint from .endpoints.organization_activity import OrganizationActivityEndpoint from .endpoints.organization_api_key_details import OrganizationApiKeyDetailsEndpoint @@ -294,6 +291,11 @@ OrganizationMetricsCompatibility, OrganizationMetricsCompatibilitySums, ) +from .endpoints.organization_monitor_checkin_attachment import ( + OrganizationMonitorCheckInAttachmentEndpoint, +) +from .endpoints.organization_monitor_details import OrganizationMonitorDetailsEndpoint +from .endpoints.organization_monitor_stats import OrganizationMonitorStatsEndpoint from .endpoints.organization_monitors import OrganizationMonitorsEndpoint from .endpoints.organization_onboarding_continuation_email import ( OrganizationOnboardingContinuationEmail, @@ -664,11 +666,6 @@ r"^monitors/", include( [ - url( - r"^(?P[^\/]+)/$", - MonitorDetailsEndpoint.as_view(), - name="sentry-api-0-monitor-details", - ), url( r"^(?P[^\/]+)/checkins/$", MonitorCheckInsEndpoint.as_view(), @@ -679,11 +676,6 @@ MonitorCheckInDetailsEndpoint.as_view(), name="sentry-api-0-monitor-check-in-details", ), - url( - r"^(?P[^\/]+)/stats/$", - MonitorStatsEndpoint.as_view(), - name="sentry-api-0-monitor-stats", - ), ] ), ), @@ -692,11 +684,6 @@ r"^organizations/(?P[^\/]+)/monitors/", include( [ - url( - r"^(?P[^\/]+)/$", - MonitorDetailsEndpoint.as_view(), - name="sentry-api-0-organization-monitor-details", - ), url( r"^(?P[^\/]+)/checkins/$", MonitorCheckInsEndpoint.as_view(), @@ -707,16 +694,6 @@ MonitorCheckInDetailsEndpoint.as_view(), name="sentry-api-0-organization-monitor-check-in-details", ), - url( - r"^(?P[^\/]+)/checkins/(?P[^\/]+)/attachment/$", - MonitorCheckInAttachmentEndpoint.as_view(), - name="sentry-api-0-organization-monitor-check-in-attachment", - ), - url( - r"^(?P[^\/]+)/stats/$", - MonitorStatsEndpoint.as_view(), - name="sentry-api-0-organization-monitor-stats", - ), ] ), ), @@ -1336,6 +1313,21 @@ OrganizationMonitorsEndpoint.as_view(), name="sentry-api-0-organization-monitors", ), + url( + r"^(?P[^\/]+)/monitors/(?P[^\/]+)/$", + OrganizationMonitorDetailsEndpoint.as_view(), + name="sentry-api-0-organization-monitor-details", + ), + url( + r"^(?P[^\/]+)/monitors/(?P[^\/]+)/stats/$", + OrganizationMonitorStatsEndpoint.as_view(), + name="sentry-api-0-organization-monitor-stats", + ), + url( + r"^(?P[^\/]+)/monitors/(?P[^\/]+)/checkins/(?P[^\/]+)/attachment/$", + OrganizationMonitorCheckInAttachmentEndpoint.as_view(), + name="sentry-api-0-organization-monitor-check-in-attachment", + ), url( r"^(?P[^\/]+)/pinned-searches/$", OrganizationPinnedSearchEndpoint.as_view(), diff --git a/src/sentry/apidocs/public_exclusion_list.py b/src/sentry/apidocs/public_exclusion_list.py index eb00f942df79b2..45a77c0d45f978 100644 --- a/src/sentry/apidocs/public_exclusion_list.py +++ b/src/sentry/apidocs/public_exclusion_list.py @@ -126,7 +126,6 @@ InternalStatsEndpoint, InternalWarningsEndpoint, ) -from sentry.api.endpoints.monitor_stats import MonitorStatsEndpoint from sentry.api.endpoints.organization_access_request_details import ( OrganizationAccessRequestDetailsEndpoint, ) @@ -231,6 +230,7 @@ OrganizationMetricsTagDetailsEndpoint, OrganizationMetricsTagsEndpoint, ) +from sentry.api.endpoints.organization_monitor_stats import OrganizationMonitorStatsEndpoint from sentry.api.endpoints.organization_monitors import OrganizationMonitorsEndpoint from sentry.api.endpoints.organization_onboarding_tasks import OrganizationOnboardingTaskEndpoint from sentry.api.endpoints.organization_pinned_searches import OrganizationPinnedSearchEndpoint @@ -617,7 +617,7 @@ BroadcastDetailsEndpoint, AcceptProjectTransferEndpoint, AcceptOrganizationInvite, - MonitorStatsEndpoint, + OrganizationMonitorStatsEndpoint, UserIndexEndpoint, UserDetailsEndpoint, UserAvatarEndpoint, diff --git a/tests/sentry/api/endpoints/test_monitor_details.py b/tests/sentry/api/endpoints/test_monitor_details.py deleted file mode 100644 index 90d116d6b0c191..00000000000000 --- a/tests/sentry/api/endpoints/test_monitor_details.py +++ /dev/null @@ -1,313 +0,0 @@ -from sentry.models import Monitor, MonitorStatus, ScheduledDeletion, ScheduleType -from sentry.testutils import MonitorTestCase -from sentry.testutils.silo import region_silo_test - - -@region_silo_test(stable=True) -class MonitorDetailsTest(MonitorTestCase): - endpoint = "sentry-api-0-monitor-details" - endpoint_with_org = "sentry-api-0-organization-monitor-details" - - def setUp(self): - super().setUp() - - def test_simple(self): - self.login_as(user=self.user) - monitor = self._create_monitor() - - for path_func in self._get_path_functions(): - path = path_func(monitor.guid) - resp = self.client.get(path) - - assert resp.status_code == 200, resp.content - assert resp.data["id"] == str(monitor.guid) - - def test_mismatched_org_slugs(self): - monitor = self._create_monitor() - path = f"/api/0/organizations/asdf/monitors/{monitor.guid}/" - self.login_as(user=self.user) - - resp = self.client.get(path) - - assert resp.status_code == 400 - - def test_invalid_monitor_id(self): - self._create_monitor() - path = "/api/0/organizations/asdf/monitors/bad-guid/" - self.login_as(user=self.user) - - resp = self.client.get(path) - - assert resp.status_code == 400 - - -@region_silo_test(stable=True) -class UpdateMonitorTest(MonitorTestCase): - endpoint = "sentry-api-0-monitor-details" - endpoint_with_org = "sentry-api-0-organization-monitor-details" - - def setUp(self): - super().setUp() - - self.login_as(user=self.user) - - def test_name(self): - monitor = self._create_monitor() - - for i, path_func in enumerate(self._get_path_functions()): - monitor = self._create_monitor() - path = path_func(monitor.guid) - resp = self.client.put(path, data={"name": f"Monitor Name {i}"}) - - assert resp.status_code == 200, resp.content - assert resp.data["id"] == str(monitor.guid) - - monitor = Monitor.objects.get(id=monitor.id) - assert monitor.name == f"Monitor Name {i}" - - def test_can_disable(self): - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - resp = self.client.put(path, data={"status": "disabled"}) - - assert resp.status_code == 200, resp.content - assert resp.data["id"] == str(monitor.guid) - - monitor = Monitor.objects.get(id=monitor.id) - assert monitor.status == MonitorStatus.DISABLED - - def test_can_enable(self): - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - - monitor.update(status=MonitorStatus.DISABLED) - - resp = self.client.put(path, data={"status": "active"}) - - assert resp.status_code == 200, resp.content - assert resp.data["id"] == str(monitor.guid) - - monitor = Monitor.objects.get(id=monitor.id) - assert monitor.status == MonitorStatus.ACTIVE - - def test_cannot_enable_if_enabled(self): - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - - monitor.update(status=MonitorStatus.OK) - - resp = self.client.put(path, data={"status": "active"}) - - assert resp.status_code == 200, resp.content - assert resp.data["id"] == str(monitor.guid) - - monitor = Monitor.objects.get(id=monitor.id) - assert monitor.status == MonitorStatus.OK - - def test_timezone(self): - monitor = self._create_monitor() - - for i, path_func in enumerate(self._get_path_functions()): - monitor = self._create_monitor() - path = path_func(monitor.guid) - resp = self.client.put(path, data={"config": {"timezone": "America/Los_Angeles"}}) - - assert resp.status_code == 200, resp.content - assert resp.data["id"] == str(monitor.guid) - - monitor = Monitor.objects.get(id=monitor.id) - assert monitor.config["timezone"] == "America/Los_Angeles" - - def test_checkin_margin(self): - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - - resp = self.client.put(path, data={"config": {"checkin_margin": 30}}) - - assert resp.status_code == 200, resp.content - assert resp.data["id"] == str(monitor.guid) - - monitor = Monitor.objects.get(id=monitor.id) - assert monitor.config["checkin_margin"] == 30 - - def test_max_runtime(self): - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - - resp = self.client.put(path, data={"config": {"max_runtime": 30}}) - - assert resp.status_code == 200, resp.content - assert resp.data["id"] == str(monitor.guid) - - monitor = Monitor.objects.get(id=monitor.id) - assert monitor.config["max_runtime"] == 30 - - def test_invalid_config_param(self): - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - - resp = self.client.put(path, data={"config": {"invalid": True}}) - - assert resp.status_code == 200, resp.content - assert resp.data["id"] == str(monitor.guid) - - monitor = Monitor.objects.get(id=monitor.id) - assert "invalid" not in monitor.config - - def test_cronjob_crontab(self): - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - - resp = self.client.put(path, data={"config": {"schedule": "*/5 * * * *"}}) - - assert resp.status_code == 200, resp.content - assert resp.data["id"] == str(monitor.guid) - - monitor = Monitor.objects.get(id=monitor.id) - assert monitor.config["schedule_type"] == ScheduleType.CRONTAB - assert monitor.config["schedule"] == "*/5 * * * *" - - # TODO(dcramer): would be lovely to run the full spectrum, but it requires - # this test to not be class-based - # @pytest.mark.parametrize('input,expected', ( - # ['@yearly', '0 0 1 1 *'], - # ['@annually', '0 0 1 1 *'], - # ['@monthly', '0 0 1 * *'], - # ['@weekly', '0 0 * * 0'], - # ['@daily', '0 0 * * *'], - # ['@hourly', '0 * * * *'], - # )) - def test_cronjob_nonstandard(self): - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - - resp = self.client.put(path, data={"config": {"schedule": "@monthly"}}) - - assert resp.status_code == 200, resp.content - assert resp.data["id"] == str(monitor.guid) - - monitor = Monitor.objects.get(id=monitor.id) - assert monitor.config["schedule_type"] == ScheduleType.CRONTAB - assert monitor.config["schedule"] == "0 0 1 * *" - - def test_cronjob_crontab_invalid(self): - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - - resp = self.client.put(path, data={"config": {"schedule": "*/0.5 * * * *"}}) - - assert resp.status_code == 400, resp.content - - resp = self.client.put(path, data={"config": {"schedule": "* * * *"}}) - - assert resp.status_code == 400, resp.content - - def test_cronjob_interval(self): - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - - resp = self.client.put( - path, data={"config": {"schedule_type": "interval", "schedule": [1, "month"]}} - ) - - assert resp.status_code == 200, resp.content - assert resp.data["id"] == str(monitor.guid) - - monitor = Monitor.objects.get(id=monitor.id) - assert monitor.config["schedule_type"] == ScheduleType.INTERVAL - assert monitor.config["schedule"] == [1, "month"] - - def test_cronjob_interval_invalid_inteval(self): - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - - resp = self.client.put( - path, data={"config": {"schedule_type": "interval", "schedule": [1, "decade"]}} - ) - - assert resp.status_code == 400, resp.content - - resp = self.client.put( - path, - data={"config": {"schedule_type": "interval", "schedule": ["foo", "month"]}}, - ) - - assert resp.status_code == 400, resp.content - - resp = self.client.put( - path, data={"config": {"schedule_type": "interval", "schedule": "bar"}} - ) - - assert resp.status_code == 400, resp.content - - def test_mismatched_org_slugs(self): - monitor = self._create_monitor() - path = f"/api/0/organizations/asdf/monitors/{monitor.guid}/" - self.login_as(user=self.user) - - resp = self.client.put( - path, data={"config": {"schedule_type": "interval", "schedule": [1, "month"]}} - ) - - assert resp.status_code == 400 - - def test_cannot_change_project(self): - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - self.login_as(user=self.user) - - project2 = self.create_project() - resp = self.client.put(path, data={"project": project2.slug}) - - assert resp.status_code == 400, resp.content - assert ( - resp.data["detail"]["message"] - == "existing monitors may not be moved between projects" - ), resp.content - - -@region_silo_test() -class DeleteMonitorTest(MonitorTestCase): - endpoint = "sentry-api-0-monitor-details" - endpoint_with_org = "sentry-api-0-organization-monitor-details" - - def setUp(self): - super().setUp() - - def test_simple(self): - self.login_as(user=self.user) - for path_func in self._get_path_functions(): - monitor = self._create_monitor() - path = path_func(monitor.guid) - - resp = self.client.delete(path) - - assert resp.status_code == 202, resp.content - - monitor = Monitor.objects.get(id=monitor.id) - assert monitor.status == MonitorStatus.PENDING_DELETION - # ScheduledDeletion only available in control silo - assert ScheduledDeletion.objects.filter( - object_id=monitor.id, model_name="Monitor" - ).exists() - - def test_mismatched_org_slugs(self): - monitor = self._create_monitor() - path = f"/api/0/organizations/asdf/monitors/{monitor.guid}/" - self.login_as(user=self.user) - - resp = self.client.delete(path) - - assert resp.status_code == 400 diff --git a/tests/sentry/api/endpoints/test_monitor_checkin_attachment.py b/tests/sentry/api/endpoints/test_organization_monitor_checkin_attachment.py similarity index 96% rename from tests/sentry/api/endpoints/test_monitor_checkin_attachment.py rename to tests/sentry/api/endpoints/test_organization_monitor_checkin_attachment.py index 0a09f756c4af2d..b759607b7b7e06 100644 --- a/tests/sentry/api/endpoints/test_monitor_checkin_attachment.py +++ b/tests/sentry/api/endpoints/test_organization_monitor_checkin_attachment.py @@ -11,7 +11,7 @@ @region_silo_test(stable=True) -class UploadMonitorCheckInAttachmentTest(APITestCase): +class OrganizationMonitorCheckInAttachmentEndpointTest(APITestCase): endpoint = "sentry-api-0-organization-monitor-check-in-attachment" def setUp(self): @@ -98,7 +98,9 @@ def test_download_no_file(self): assert resp.status_code == 404 assert resp.data["detail"] == "Check-in has no attachment" - @mock.patch("sentry.api.endpoints.monitor_checkin_attachment.MAX_ATTACHMENT_SIZE", 1) + @mock.patch( + "sentry.api.endpoints.organization_monitor_checkin_attachment.MAX_ATTACHMENT_SIZE", 1 + ) def test_upload_file_too_big(self): monitor = self._create_monitor() checkin = MonitorCheckIn.objects.create( diff --git a/tests/sentry/api/endpoints/test_organization_monitor_details.py b/tests/sentry/api/endpoints/test_organization_monitor_details.py new file mode 100644 index 00000000000000..f16fe2e21923e9 --- /dev/null +++ b/tests/sentry/api/endpoints/test_organization_monitor_details.py @@ -0,0 +1,284 @@ +from sentry.models import Monitor, MonitorStatus, ScheduledDeletion, ScheduleType +from sentry.testutils import MonitorTestCase +from sentry.testutils.silo import region_silo_test + + +@region_silo_test(stable=True) +class OrganizationMonitorDetailsTest(MonitorTestCase): + endpoint = "sentry-api-0-organization-monitor-details" + + def setUp(self): + self.login_as(user=self.user) + super().setUp() + + def test_simple(self): + monitor = self._create_monitor() + + resp = self.get_success_response(self.organization.slug, monitor.guid) + assert resp.data["id"] == str(monitor.guid) + + def test_mismatched_org_slugs(self): + monitor = self._create_monitor() + self.get_error_response("asdf", monitor.guid, status_code=400) + + def test_invalid_monitor_id(self): + self._create_monitor() + self.get_error_response(self.organization.slug, "bad-guid", status_code=400) + + +@region_silo_test(stable=True) +class UpdateMonitorTest(MonitorTestCase): + endpoint = "sentry-api-0-organization-monitor-details" + + def setUp(self): + super().setUp() + self.login_as(user=self.user) + + def test_name(self): + monitor = self._create_monitor() + resp = self.get_success_response( + self.organization.slug, monitor.guid, method="PUT", **{"name": "Monitor Name"} + ) + assert resp.data["id"] == str(monitor.guid) + + monitor = Monitor.objects.get(id=monitor.id) + assert monitor.name == "Monitor Name" + + def test_can_disable(self): + monitor = self._create_monitor() + resp = self.get_success_response( + self.organization.slug, monitor.guid, method="PUT", **{"status": "disabled"} + ) + assert resp.data["id"] == str(monitor.guid) + + monitor = Monitor.objects.get(id=monitor.id) + assert monitor.status == MonitorStatus.DISABLED + + def test_can_enable(self): + monitor = self._create_monitor() + + monitor.update(status=MonitorStatus.DISABLED) + + resp = self.get_success_response( + self.organization.slug, monitor.guid, method="PUT", **{"status": "active"} + ) + assert resp.data["id"] == str(monitor.guid) + + monitor = Monitor.objects.get(id=monitor.id) + assert monitor.status == MonitorStatus.ACTIVE + + def test_cannot_enable_if_enabled(self): + monitor = self._create_monitor() + + monitor.update(status=MonitorStatus.OK) + + resp = self.get_success_response( + self.organization.slug, monitor.guid, method="PUT", **{"status": "active"} + ) + assert resp.data["id"] == str(monitor.guid) + + monitor = Monitor.objects.get(id=monitor.id) + assert monitor.status == MonitorStatus.OK + + def test_timezone(self): + monitor = self._create_monitor() + + resp = self.get_success_response( + self.organization.slug, + monitor.guid, + method="PUT", + **{"config": {"timezone": "America/Los_Angeles"}}, + ) + assert resp.data["id"] == str(monitor.guid) + + monitor = Monitor.objects.get(id=monitor.id) + assert monitor.config["timezone"] == "America/Los_Angeles" + + def test_checkin_margin(self): + monitor = self._create_monitor() + + resp = self.get_success_response( + self.organization.slug, + monitor.guid, + method="PUT", + **{"config": {"checkin_margin": 30}}, + ) + assert resp.data["id"] == str(monitor.guid) + + monitor = Monitor.objects.get(id=monitor.id) + assert monitor.config["checkin_margin"] == 30 + + def test_max_runtime(self): + monitor = self._create_monitor() + + resp = self.get_success_response( + self.organization.slug, monitor.guid, method="PUT", **{"config": {"max_runtime": 30}} + ) + assert resp.data["id"] == str(monitor.guid) + + monitor = Monitor.objects.get(id=monitor.id) + assert monitor.config["max_runtime"] == 30 + + def test_invalid_config_param(self): + monitor = self._create_monitor() + + resp = self.get_success_response( + self.organization.slug, monitor.guid, method="PUT", **{"config": {"invalid": True}} + ) + assert resp.data["id"] == str(monitor.guid) + + monitor = Monitor.objects.get(id=monitor.id) + assert "invalid" not in monitor.config + + def test_cronjob_crontab(self): + monitor = self._create_monitor() + + resp = self.get_success_response( + self.organization.slug, + monitor.guid, + method="PUT", + **{"config": {"schedule": "*/5 * * * *"}}, + ) + assert resp.data["id"] == str(monitor.guid) + + monitor = Monitor.objects.get(id=monitor.id) + assert monitor.config["schedule_type"] == ScheduleType.CRONTAB + assert monitor.config["schedule"] == "*/5 * * * *" + + # TODO(dcramer): would be lovely to run the full spectrum, but it requires + # this test to not be class-based + # @pytest.mark.parametrize('input,expected', ( + # ['@yearly', '0 0 1 1 *'], + # ['@annually', '0 0 1 1 *'], + # ['@monthly', '0 0 1 * *'], + # ['@weekly', '0 0 * * 0'], + # ['@daily', '0 0 * * *'], + # ['@hourly', '0 * * * *'], + # )) + def test_cronjob_nonstandard(self): + monitor = self._create_monitor() + + resp = self.get_success_response( + self.organization.slug, + monitor.guid, + method="PUT", + **{"config": {"schedule": "@monthly"}}, + ) + assert resp.data["id"] == str(monitor.guid) + + monitor = Monitor.objects.get(id=monitor.id) + assert monitor.config["schedule_type"] == ScheduleType.CRONTAB + assert monitor.config["schedule"] == "0 0 1 * *" + + def test_cronjob_crontab_invalid(self): + monitor = self._create_monitor() + + self.get_error_response( + self.organization.slug, + monitor.guid, + method="PUT", + status_code=400, + **{"config": {"schedule": "*/0.5 * * * *"}}, + ) + self.get_error_response( + self.organization.slug, + monitor.guid, + method="PUT", + status_code=400, + **{"config": {"schedule": "* * * *"}}, + ) + + def test_cronjob_interval(self): + monitor = self._create_monitor() + + resp = self.get_success_response( + self.organization.slug, + monitor.guid, + method="PUT", + **{"config": {"schedule_type": "interval", "schedule": [1, "month"]}}, + ) + + assert resp.data["id"] == str(monitor.guid) + + monitor = Monitor.objects.get(id=monitor.id) + assert monitor.config["schedule_type"] == ScheduleType.INTERVAL + assert monitor.config["schedule"] == [1, "month"] + + def test_cronjob_interval_invalid_inteval(self): + monitor = self._create_monitor() + + self.get_error_response( + self.organization.slug, + monitor.guid, + method="PUT", + status_code=400, + **{"config": {"schedule_type": "interval", "schedule": [1, "decade"]}}, + ) + + self.get_error_response( + self.organization.slug, + monitor.guid, + method="PUT", + status_code=400, + **{"config": {"schedule_type": "interval", "schedule": ["foo", "month"]}}, + ) + + self.get_error_response( + self.organization.slug, + monitor.guid, + method="PUT", + status_code=400, + **{"config": {"schedule_type": "interval", "schedule": "bar"}}, + ) + + def test_mismatched_org_slugs(self): + monitor = self._create_monitor() + + self.get_error_response( + "asdf", + monitor.guid, + method="PUT", + status_code=400, + **{"config": {"schedule_type": "interval", "schedule": [1, "month"]}}, + ) + + def test_cannot_change_project(self): + monitor = self._create_monitor() + + project2 = self.create_project() + resp = self.get_error_response( + self.organization.slug, + monitor.guid, + method="PUT", + status_code=400, + **{"project": project2.slug}, + ) + + assert ( + resp.data["detail"]["message"] == "existing monitors may not be moved between projects" + ), resp.content + + +@region_silo_test() +class DeleteMonitorTest(MonitorTestCase): + endpoint = "sentry-api-0-organization-monitor-details" + + def setUp(self): + self.login_as(user=self.user) + super().setUp() + + def test_simple(self): + monitor = self._create_monitor() + + self.get_success_response( + self.organization.slug, monitor.guid, method="DELETE", status_code=202 + ) + + monitor = Monitor.objects.get(id=monitor.id) + assert monitor.status == MonitorStatus.PENDING_DELETION + # ScheduledDeletion only available in control silo + assert ScheduledDeletion.objects.filter(object_id=monitor.id, model_name="Monitor").exists() + + def test_mismatched_org_slugs(self): + monitor = self._create_monitor() + self.get_error_response("asdf", monitor.guid, status_code=400)