test_project_rule_details.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. from datetime import datetime
  2. from typing import Any, Mapping
  3. from unittest.mock import patch
  4. import responses
  5. from freezegun import freeze_time
  6. from pytz import UTC
  7. from sentry.integrations.slack.utils.channel import strip_channel_name
  8. from sentry.models import (
  9. Environment,
  10. Integration,
  11. Rule,
  12. RuleActivity,
  13. RuleActivityType,
  14. RuleFireHistory,
  15. RuleStatus,
  16. User,
  17. )
  18. from sentry.testutils import APITestCase
  19. from sentry.testutils.helpers import install_slack
  20. from sentry.utils import json
  21. def assert_rule_from_payload(rule: Rule, payload: Mapping[str, Any]) -> None:
  22. """
  23. Helper function to assert every field on a Rule was modified correctly from the incoming payload
  24. """
  25. rule.refresh_from_db()
  26. assert rule.label == payload.get("name")
  27. owner_id = payload.get("owner")
  28. if owner_id:
  29. assert rule.owner == User.objects.get(id=owner_id).actor
  30. else:
  31. assert rule.owner is None
  32. environment = payload.get("environment")
  33. if environment:
  34. assert (
  35. rule.environment_id
  36. == Environment.objects.get(projects=rule.project, name=environment).id
  37. )
  38. else:
  39. assert rule.environment_id is None
  40. assert rule.data["action_match"] == payload.get("actionMatch")
  41. assert rule.data["filter_match"] == payload.get("filterMatch")
  42. # For actions/conditions/filters, payload might only have a portion of the rule data so we use
  43. # any(a.items() <= b.items()) to check if the payload dict is a subset of the rule.data dict
  44. # E.g. payload["actions"] = [{"name": "Test1"}], rule.data["actions"] = [{"name": "Test1", "id": 1}]
  45. for payload_action in payload.get("actions", []):
  46. # The Slack payload will contain '#channel' or '@user', but we save 'channel' or 'user' on the Rule
  47. if (
  48. payload_action["id"]
  49. == "sentry.integrations.slack.notify_action.SlackNotifyServiceAction"
  50. ):
  51. payload_action["channel"] = strip_channel_name(payload_action["channel"])
  52. assert any(
  53. payload_action.items() <= rule_action.items() for rule_action in rule.data["actions"]
  54. )
  55. payload_conditions = payload.get("conditions", []) + payload.get("filters", [])
  56. for payload_condition in payload_conditions:
  57. assert any(
  58. payload_condition.items() <= rule_condition.items()
  59. for rule_condition in rule.data["conditions"]
  60. )
  61. assert RuleActivity.objects.filter(rule=rule, type=RuleActivityType.UPDATED.value).exists()
  62. class ProjectRuleDetailsBaseTestCase(APITestCase):
  63. endpoint = "sentry-api-0-project-rule-details"
  64. def setUp(self):
  65. self.rule = self.create_project_rule(project=self.project)
  66. self.environment = self.create_environment(self.project, name="production")
  67. self.slack_integration = install_slack(organization=self.organization)
  68. self.jira_integration = Integration.objects.create(
  69. provider="jira", name="Jira", external_id="jira:1"
  70. )
  71. self.jira_integration.add_organization(self.organization, self.user)
  72. self.sentry_app = self.create_sentry_app(
  73. name="Pied Piper",
  74. organization=self.organization,
  75. schema={"elements": [self.create_alert_rule_action_schema()]},
  76. )
  77. self.sentry_app_installation = self.create_sentry_app_installation(
  78. slug=self.sentry_app.slug, organization=self.organization
  79. )
  80. self.sentry_app_settings_payload = [
  81. {"name": "title", "value": "Team Rocket"},
  82. {"name": "summary", "value": "We're blasting off again."},
  83. ]
  84. self.login_as(self.user)
  85. class ProjectRuleDetailsTest(ProjectRuleDetailsBaseTestCase):
  86. def test_simple(self):
  87. response = self.get_success_response(
  88. self.organization.slug, self.project.slug, self.rule.id, status_code=200
  89. )
  90. assert response.data["id"] == str(self.rule.id)
  91. assert response.data["environment"] is None
  92. def test_non_existing_rule(self):
  93. self.get_error_response(self.organization.slug, self.project.slug, 12345, status_code=404)
  94. def test_with_environment(self):
  95. self.rule.update(environment_id=self.environment.id)
  96. response = self.get_success_response(
  97. self.organization.slug, self.project.slug, self.rule.id, status_code=200
  98. )
  99. assert response.data["id"] == str(self.rule.id)
  100. assert response.data["environment"] == self.environment.name
  101. def test_with_filters(self):
  102. conditions = [
  103. {"id": "sentry.rules.conditions.every_event.EveryEventCondition"},
  104. {"id": "sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter", "value": 10},
  105. ]
  106. actions = [{"id": "sentry.rules.actions.notify_event.NotifyEventAction"}]
  107. data = {
  108. "conditions": conditions,
  109. "actions": actions,
  110. "filter_match": "all",
  111. "action_match": "all",
  112. "frequency": 30,
  113. }
  114. self.rule.update(data=data)
  115. response = self.get_success_response(
  116. self.organization.slug, self.project.slug, self.rule.id, status_code=200
  117. )
  118. assert response.data["id"] == str(self.rule.id)
  119. # ensure that conditions and filters are split up correctly
  120. assert len(response.data["conditions"]) == 1
  121. assert response.data["conditions"][0]["id"] == conditions[0]["id"]
  122. assert len(response.data["filters"]) == 1
  123. assert response.data["filters"][0]["id"] == conditions[1]["id"]
  124. @responses.activate
  125. def test_with_unresponsive_sentryapp(self):
  126. conditions = [
  127. {"id": "sentry.rules.conditions.every_event.EveryEventCondition"},
  128. {"id": "sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter", "value": 10},
  129. ]
  130. actions = [
  131. {
  132. "id": "sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction",
  133. "sentryAppInstallationUuid": self.sentry_app_installation.uuid,
  134. "settings": [
  135. {"name": "title", "value": "An alert"},
  136. {"summary": "Something happened here..."},
  137. {"name": "points", "value": "3"},
  138. {"name": "assignee", "value": "Nisanthan"},
  139. ],
  140. }
  141. ]
  142. data = {
  143. "conditions": conditions,
  144. "actions": actions,
  145. "filter_match": "all",
  146. "action_match": "all",
  147. "frequency": 30,
  148. }
  149. self.rule.update(data=data)
  150. responses.add(responses.GET, "http://example.com/sentry/members", json={}, status=404)
  151. response = self.get_success_response(
  152. self.organization.slug, self.project.slug, self.rule.id, status_code=200
  153. )
  154. assert len(responses.calls) == 1
  155. assert response.status_code == 200
  156. # Returns errors while fetching
  157. assert len(response.data["errors"]) == 1
  158. assert self.sentry_app.name in response.data["errors"][0]["detail"]
  159. # Disables the SentryApp
  160. assert (
  161. response.data["actions"][0]["sentryAppInstallationUuid"]
  162. == self.sentry_app_installation.uuid
  163. )
  164. assert response.data["actions"][0]["disabled"] is True
  165. @freeze_time()
  166. def test_last_triggered(self):
  167. response = self.get_success_response(
  168. self.organization.slug, self.project.slug, self.rule.id, expand=["lastTriggered"]
  169. )
  170. assert response.data["lastTriggered"] is None
  171. RuleFireHistory.objects.create(project=self.project, rule=self.rule, group=self.group)
  172. response = self.get_success_response(
  173. self.organization.slug, self.project.slug, self.rule.id, expand=["lastTriggered"]
  174. )
  175. assert response.data["lastTriggered"] == datetime.now().replace(tzinfo=UTC)
  176. def test_with_jira_action_error(self):
  177. conditions = [
  178. {"id": "sentry.rules.conditions.every_event.EveryEventCondition"},
  179. {"id": "sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter", "value": 10},
  180. ]
  181. actions = [
  182. {
  183. "id": "sentry.integrations.jira.notify_action.JiraCreateTicketAction",
  184. "integration": self.jira_integration.id,
  185. "customfield_epic_link": "EPIC-3",
  186. "customfield_severity": "Medium",
  187. "dynamic_form_fields": [
  188. {
  189. "choices": [
  190. ["EPIC-1", "Citizen Knope"],
  191. ["EPIC-2", "The Comeback Kid"],
  192. ["EPIC-3", {"key": None, "ref": None, "props": {}, "_owner": None}],
  193. ],
  194. "label": "Epic Link",
  195. "name": "customfield_epic_link",
  196. "required": False,
  197. "type": "select",
  198. "url": f"/extensions/jira/search/{self.organization.slug}/{self.jira_integration.id}/",
  199. },
  200. {
  201. "choices": [
  202. ["Very High", "Very High"],
  203. ["High", "High"],
  204. ["Medium", "Medium"],
  205. ["Low", "Low"],
  206. ],
  207. "label": "Severity",
  208. "name": "customfield_severity",
  209. "required": True,
  210. "type": "select",
  211. },
  212. ],
  213. }
  214. ]
  215. data = {
  216. "conditions": conditions,
  217. "actions": actions,
  218. "filter_match": "all",
  219. "action_match": "all",
  220. "frequency": 30,
  221. }
  222. self.rule.update(data=data)
  223. response = self.get_success_response(
  224. self.organization.slug, self.project.slug, self.rule.id, status_code=200
  225. )
  226. # Expect that the choices get filtered to match the API: Array<string, string>
  227. assert response.data["actions"][0].get("dynamic_form_fields")[0].get("choices") == [
  228. ["EPIC-1", "Citizen Knope"],
  229. ["EPIC-2", "The Comeback Kid"],
  230. ]
  231. class UpdateProjectRuleTest(ProjectRuleDetailsBaseTestCase):
  232. method = "PUT"
  233. @patch("sentry.signals.alert_rule_edited.send_robust")
  234. def test_simple(self, send_robust):
  235. conditions = [
  236. {
  237. "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition",
  238. "key": "foo",
  239. "match": "eq",
  240. "value": "bar",
  241. }
  242. ]
  243. payload = {
  244. "name": "hello world",
  245. "owner": self.user.id,
  246. "actionMatch": "any",
  247. "filterMatch": "any",
  248. "actions": [{"id": "sentry.rules.actions.notify_event.NotifyEventAction"}],
  249. "conditions": conditions,
  250. }
  251. response = self.get_success_response(
  252. self.organization.slug, self.project.slug, self.rule.id, status_code=200, **payload
  253. )
  254. assert response.data["id"] == str(self.rule.id)
  255. assert_rule_from_payload(self.rule, payload)
  256. assert send_robust.called
  257. def test_no_owner(self):
  258. conditions = [
  259. {
  260. "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition",
  261. "key": "foo",
  262. "match": "eq",
  263. "value": "bar",
  264. }
  265. ]
  266. payload = {
  267. "name": "hello world",
  268. "owner": None,
  269. "actionMatch": "any",
  270. "filterMatch": "any",
  271. "actions": [{"id": "sentry.rules.actions.notify_event.NotifyEventAction"}],
  272. "conditions": conditions,
  273. }
  274. response = self.get_success_response(
  275. self.organization.slug, self.project.slug, self.rule.id, status_code=200, **payload
  276. )
  277. assert response.data["id"] == str(self.rule.id)
  278. assert_rule_from_payload(self.rule, payload)
  279. def test_update_name(self):
  280. conditions = [
  281. {
  282. "interval": "1h",
  283. "id": "sentry.rules.conditions.event_frequency.EventFrequencyCondition",
  284. "value": 666,
  285. "name": "The issue is seen more than 30 times in 1m",
  286. }
  287. ]
  288. actions = [
  289. {
  290. "id": "sentry.rules.actions.notify_event.NotifyEventAction",
  291. "name": "Send a notification (for all legacy integrations)",
  292. }
  293. ]
  294. payload = {
  295. "name": "test",
  296. "environment": None,
  297. "actionMatch": "all",
  298. "filterMatch": "all",
  299. "frequency": 30,
  300. "conditions": conditions,
  301. "actions": actions,
  302. }
  303. response = self.get_success_response(
  304. self.organization.slug, self.project.slug, self.rule.id, status_code=200, **payload
  305. )
  306. assert (
  307. response.data["conditions"][0]["name"] == "The issue is seen more than 666 times in 1h"
  308. )
  309. assert_rule_from_payload(self.rule, payload)
  310. def test_with_environment(self):
  311. payload = {
  312. "name": "hello world",
  313. "environment": self.environment.name,
  314. "actionMatch": "any",
  315. "filterMatch": "any",
  316. "actions": [{"id": "sentry.rules.actions.notify_event.NotifyEventAction"}],
  317. "conditions": [
  318. {"id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}
  319. ],
  320. }
  321. response = self.get_success_response(
  322. self.organization.slug, self.project.slug, self.rule.id, status_code=200, **payload
  323. )
  324. assert response.data["id"] == str(self.rule.id)
  325. assert response.data["environment"] == self.environment.name
  326. assert_rule_from_payload(self.rule, payload)
  327. def test_with_null_environment(self):
  328. self.rule.update(environment_id=self.environment.id)
  329. payload = {
  330. "name": "hello world",
  331. "environment": None,
  332. "actionMatch": "any",
  333. "filterMatch": "any",
  334. "actions": [{"id": "sentry.rules.actions.notify_event.NotifyEventAction"}],
  335. "conditions": [
  336. {"id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}
  337. ],
  338. }
  339. response = self.get_success_response(
  340. self.organization.slug, self.project.slug, self.rule.id, status_code=200, **payload
  341. )
  342. assert response.data["id"] == str(self.rule.id)
  343. assert response.data["environment"] is None
  344. assert_rule_from_payload(self.rule, payload)
  345. @responses.activate
  346. def test_update_channel_slack(self):
  347. conditions = [{"id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}]
  348. actions = [
  349. {
  350. "channel_id": "old_channel_id",
  351. "workspace": str(self.slack_integration.id),
  352. "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction",
  353. "channel": "#old_channel_name",
  354. }
  355. ]
  356. self.rule.update(data={"conditions": conditions, "actions": actions})
  357. actions[0]["channel"] = "#new_channel_name"
  358. actions[0]["channel_id"] = "new_channel_id"
  359. channels = {
  360. "ok": "true",
  361. "channels": [
  362. {"name": "old_channel_name", "id": "old_channel_id"},
  363. {"name": "new_channel_name", "id": "new_channel_id"},
  364. ],
  365. }
  366. responses.add(
  367. method=responses.GET,
  368. url="https://slack.com/api/conversations.list",
  369. status=200,
  370. content_type="application/json",
  371. body=json.dumps(channels),
  372. )
  373. responses.add(
  374. method=responses.GET,
  375. url="https://slack.com/api/conversations.info",
  376. status=200,
  377. content_type="application/json",
  378. body=json.dumps({"ok": channels["ok"], "channel": channels["channels"][1]}),
  379. )
  380. payload = {
  381. "name": "#new_channel_name",
  382. "actionMatch": "any",
  383. "filterMatch": "any",
  384. "actions": actions,
  385. "conditions": conditions,
  386. "frequency": 30,
  387. }
  388. self.get_success_response(
  389. self.organization.slug, self.project.slug, self.rule.id, status_code=200, **payload
  390. )
  391. assert_rule_from_payload(self.rule, payload)
  392. @responses.activate
  393. def test_update_channel_slack_workspace_fail(self):
  394. conditions = [{"id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}]
  395. actions = [
  396. {
  397. "channel_id": "old_channel_id",
  398. "workspace": str(self.slack_integration.id),
  399. "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction",
  400. "channel": "#old_channel_name",
  401. }
  402. ]
  403. self.rule.update(data={"conditions": conditions, "actions": actions})
  404. channels = {
  405. "ok": "true",
  406. "channels": [
  407. {"name": "old_channel_name", "id": "old_channel_id"},
  408. {"name": "new_channel_name", "id": "new_channel_id"},
  409. ],
  410. }
  411. responses.add(
  412. method=responses.GET,
  413. url="https://slack.com/api/conversations.list",
  414. status=200,
  415. content_type="application/json",
  416. body=json.dumps(channels),
  417. )
  418. responses.add(
  419. method=responses.GET,
  420. url="https://slack.com/api/conversations.info",
  421. status=200,
  422. content_type="application/json",
  423. body=json.dumps({"ok": channels["ok"], "channel": channels["channels"][0]}),
  424. )
  425. actions[0]["channel"] = "#new_channel_name"
  426. payload = {
  427. "name": "#new_channel_name",
  428. "actionMatch": "any",
  429. "filterMatch": "any",
  430. "actions": actions,
  431. "conditions": conditions,
  432. "frequency": 30,
  433. }
  434. self.get_error_response(
  435. self.organization.slug, self.project.slug, self.rule.id, status_code=400, **payload
  436. )
  437. @responses.activate
  438. def test_slack_channel_id_saved(self):
  439. channel_id = "CSVK0921"
  440. responses.add(
  441. method=responses.GET,
  442. url="https://slack.com/api/conversations.info",
  443. status=200,
  444. content_type="application/json",
  445. body=json.dumps(
  446. {"ok": "true", "channel": {"name": "team-team-team", "id": channel_id}}
  447. ),
  448. )
  449. payload = {
  450. "name": "hello world",
  451. "environment": None,
  452. "actionMatch": "any",
  453. "actions": [
  454. {
  455. "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction",
  456. "name": "Send a notification to the funinthesun Slack workspace to #team-team-team and show tags [] in notification",
  457. "workspace": str(self.slack_integration.id),
  458. "channel": "#team-team-team",
  459. "channel_id": channel_id,
  460. }
  461. ],
  462. "conditions": [
  463. {"id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}
  464. ],
  465. }
  466. response = self.get_success_response(
  467. self.organization.slug, self.project.slug, self.rule.id, status_code=200, **payload
  468. )
  469. assert response.data["id"] == str(self.rule.id)
  470. assert response.data["actions"][0]["channel_id"] == channel_id
  471. def test_invalid_rule_node_type(self):
  472. payload = {
  473. "name": "hello world",
  474. "actionMatch": "any",
  475. "filterMatch": "any",
  476. "conditions": [{"id": "sentry.rules.actions.notify_event.NotifyEventAction"}],
  477. "actions": [],
  478. }
  479. self.get_error_response(
  480. self.organization.slug, self.project.slug, self.rule.id, status_code=400, **payload
  481. )
  482. def test_invalid_rule_node(self):
  483. payload = {
  484. "name": "hello world",
  485. "actionMatch": "any",
  486. "filterMatch": "any",
  487. "conditions": [{"id": "sentry.rules.actions.notify_event.NotifyEventAction"}],
  488. "actions": [{"id": "foo"}],
  489. }
  490. self.get_error_response(
  491. self.organization.slug, self.project.slug, self.rule.id, status_code=400, **payload
  492. )
  493. def test_rule_form_not_valid(self):
  494. payload = {
  495. "name": "hello world",
  496. "actionMatch": "any",
  497. "filterMatch": "any",
  498. "conditions": [{"id": "sentry.rules.conditions.tagged_event.TaggedEventCondition"}],
  499. "actions": [],
  500. }
  501. self.get_error_response(
  502. self.organization.slug, self.project.slug, self.rule.id, status_code=400, **payload
  503. )
  504. def test_rule_form_owner_perms(self):
  505. new_user = self.create_user()
  506. payload = {
  507. "name": "hello world",
  508. "actionMatch": "any",
  509. "filterMatch": "any",
  510. "conditions": [{"id": "sentry.rules.conditions.tagged_event.TaggedEventCondition"}],
  511. "actions": [],
  512. "owner": new_user.actor.get_actor_identifier(),
  513. }
  514. response = self.get_error_response(
  515. self.organization.slug, self.project.slug, self.rule.id, status_code=400, **payload
  516. )
  517. assert str(response.data["owner"][0]) == "User is not a member of this organization"
  518. def test_rule_form_missing_action(self):
  519. payload = {
  520. "name": "hello world",
  521. "actionMatch": "any",
  522. "filterMatch": "any",
  523. "action": [],
  524. "conditions": [{"id": "sentry.rules.conditions.tagged_event.TaggedEventCondition"}],
  525. }
  526. self.get_error_response(
  527. self.organization.slug, self.project.slug, self.rule.id, status_code=400, **payload
  528. )
  529. def test_update_filters(self):
  530. conditions = [{"id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}]
  531. filters = [
  532. {"id": "sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter", "value": 10}
  533. ]
  534. payload = {
  535. "name": "hello world",
  536. "actionMatch": "any",
  537. "filterMatch": "any",
  538. "actions": [{"id": "sentry.rules.actions.notify_event.NotifyEventAction"}],
  539. "conditions": conditions,
  540. "filters": filters,
  541. }
  542. response = self.get_success_response(
  543. self.organization.slug, self.project.slug, self.rule.id, status_code=200, **payload
  544. )
  545. assert response.data["id"] == str(self.rule.id)
  546. assert_rule_from_payload(self.rule, payload)
  547. @responses.activate
  548. def test_update_sentry_app_action_success(self):
  549. responses.add(
  550. method=responses.POST,
  551. url="https://example.com/sentry/alert-rule",
  552. status=202,
  553. )
  554. actions = [
  555. {
  556. "id": "sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction",
  557. "settings": self.sentry_app_settings_payload,
  558. "sentryAppInstallationUuid": self.sentry_app_installation.uuid,
  559. "hasSchemaFormConfig": True,
  560. },
  561. ]
  562. payload = {
  563. "name": "my super cool rule",
  564. "actionMatch": "any",
  565. "filterMatch": "any",
  566. "actions": actions,
  567. "conditions": [],
  568. "filters": [],
  569. }
  570. self.get_success_response(
  571. self.organization.slug, self.project.slug, self.rule.id, status_code=200, **payload
  572. )
  573. assert_rule_from_payload(self.rule, payload)
  574. assert len(responses.calls) == 1
  575. @responses.activate
  576. def test_update_sentry_app_action_failure(self):
  577. error_message = "Something is totally broken :'("
  578. responses.add(
  579. method=responses.POST,
  580. url="https://example.com/sentry/alert-rule",
  581. status=500,
  582. json={"message": error_message},
  583. )
  584. actions = [
  585. {
  586. "id": "sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction",
  587. "settings": self.sentry_app_settings_payload,
  588. "sentryAppInstallationUuid": self.sentry_app_installation.uuid,
  589. "hasSchemaFormConfig": True,
  590. },
  591. ]
  592. payload = {
  593. "name": "my super cool rule",
  594. "actionMatch": "any",
  595. "filterMatch": "any",
  596. "actions": actions,
  597. "conditions": [],
  598. "filters": [],
  599. }
  600. response = self.get_error_response(
  601. self.organization.slug, self.project.slug, self.rule.id, status_code=400, **payload
  602. )
  603. assert len(responses.calls) == 1
  604. assert error_message in response.json().get("actions")[0]
  605. class DeleteProjectRuleTest(ProjectRuleDetailsBaseTestCase):
  606. method = "DELETE"
  607. def test_simple(self):
  608. self.get_success_response(
  609. self.organization.slug, self.project.slug, self.rule.id, status_code=202
  610. )
  611. self.rule.refresh_from_db()
  612. assert self.rule.status == RuleStatus.PENDING_DELETION
  613. assert RuleActivity.objects.filter(
  614. rule=self.rule, type=RuleActivityType.DELETED.value
  615. ).exists()