tests.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. import json
  2. import random
  3. from unittest.mock import patch
  4. from django.shortcuts import reverse
  5. from django.test import override_settings
  6. from model_bakery import baker
  7. from rest_framework.test import APITestCase
  8. from environments.models import Environment
  9. from glitchtip import test_utils # pylint: disable=unused-import
  10. from issues.models import EventStatus, Issue
  11. from ..models import Event, LogLevel
  12. from ..test_data.csp import mdn_sample_csp
  13. class EventStoreTestCase(APITestCase):
  14. def setUp(self):
  15. self.project = baker.make("projects.Project")
  16. self.projectkey = self.project.projectkey_set.first()
  17. self.params = f"?sentry_key={self.projectkey.public_key}"
  18. self.url = reverse("event_store", args=[self.project.id]) + self.params
  19. def test_store_api(self):
  20. with open("events/test_data/py_hi_event.json") as json_file:
  21. data = json.load(json_file)
  22. res = self.client.post(self.url, data, format="json")
  23. self.assertEqual(res.status_code, 200)
  24. def test_maintenance_freeze(self):
  25. with open("events/test_data/py_hi_event.json") as json_file:
  26. data = json.load(json_file)
  27. with override_settings(MAINTENANCE_EVENT_FREEZE=True):
  28. res = self.client.post(self.url, data, format="json")
  29. self.assertEqual(res.status_code, 503)
  30. def test_store_duplicate(self):
  31. with open("events/test_data/py_hi_event.json") as json_file:
  32. data = json.load(json_file)
  33. self.client.post(self.url, data, format="json")
  34. res = self.client.post(self.url, data, format="json")
  35. self.assertContains(res, "ID already exist", status_code=403)
  36. def test_store_invalid_key(self):
  37. with open("events/test_data/py_hi_event.json") as json_file:
  38. data = json.load(json_file)
  39. self.client.post(self.url, data, format="json")
  40. res = self.client.post(self.url, data, format="json")
  41. self.assertContains(res, "ID already exist", status_code=403)
  42. def test_store_api_auth_failure(self):
  43. url = "/api/1/store/"
  44. with open("events/test_data/py_hi_event.json") as json_file:
  45. data = json.load(json_file)
  46. params = "?sentry_key=aaa"
  47. url = reverse("event_store", args=[self.project.id]) + params
  48. res = self.client.post(url, data, format="json")
  49. self.assertEqual(res.status_code, 401)
  50. params = "?sentry_key=238df2aac6331578a16c14bcb3db5259"
  51. url = reverse("event_store", args=[self.project.id]) + params
  52. res = self.client.post(url, data, format="json")
  53. self.assertContains(res, "Invalid api key", status_code=401)
  54. url = reverse("event_store", args=[10000]) + self.params
  55. res = self.client.post(url, data, format="json")
  56. self.assertContains(res, "Invalid", status_code=401)
  57. def test_error_event(self):
  58. with open("events/test_data/py_error.json") as json_file:
  59. data = json.load(json_file)
  60. res = self.client.post(self.url, data, format="json")
  61. self.assertEqual(res.status_code, 200)
  62. def test_csp_event(self):
  63. url = reverse("csp_store", args=[self.project.id]) + self.params
  64. data = mdn_sample_csp
  65. res = self.client.post(url, data, format="json")
  66. self.assertEqual(res.status_code, 200)
  67. expected_title = "Blocked 'style' from 'example.com'"
  68. issue = Issue.objects.get(title=expected_title)
  69. event = Event.objects.get()
  70. self.assertEqual(event.data["csp"]["effective_directive"], "style-src")
  71. self.assertTrue(issue)
  72. def test_reopen_resolved_issue(self):
  73. with open("events/test_data/py_hi_event.json") as json_file:
  74. data = json.load(json_file)
  75. self.client.post(self.url, data, format="json")
  76. issue = Issue.objects.all().first()
  77. issue.status = EventStatus.RESOLVED
  78. issue.save()
  79. data["event_id"] = "6600a066e64b4caf8ed7ec5af64ac4ba"
  80. self.client.post(self.url, data, format="json")
  81. issue.refresh_from_db()
  82. self.assertEqual(issue.status, EventStatus.UNRESOLVED)
  83. def test_performance(self):
  84. with open("events/test_data/py_hi_event.json") as json_file:
  85. data = json.load(json_file)
  86. with self.assertNumQueries(9):
  87. res = self.client.post(self.url, data, format="json")
  88. self.assertEqual(res.status_code, 200)
  89. # Second event should have less queries
  90. data["event_id"] = "6600a066e64b4caf8ed7ec5af64ac4bb"
  91. with self.assertNumQueries(6):
  92. res = self.client.post(self.url, data, format="json")
  93. self.assertEqual(res.status_code, 200)
  94. @override_settings(CELERY_TASK_ALWAYS_EAGER=False)
  95. def test_ingest_response_performance(self):
  96. """Only test sync actions, not celery"""
  97. with open("events/test_data/py_hi_event.json") as json_file:
  98. data = json.load(json_file)
  99. with self.assertNumQueries(7):
  100. res = self.client.post(self.url, data, format="json")
  101. self.assertEqual(res.status_code, 200)
  102. # Second event should have less queries
  103. data["event_id"] = "6600a066e64b4caf8ed7ec5af64ac4bb"
  104. with self.assertNumQueries(5):
  105. res = self.client.post(self.url, data, format="json")
  106. self.assertEqual(res.status_code, 200)
  107. def test_throttle_organization(self):
  108. organization = self.project.organization
  109. organization.is_accepting_events = False
  110. organization.save()
  111. with open("events/test_data/py_hi_event.json") as json_file:
  112. data = json.load(json_file)
  113. res = self.client.post(self.url, data, format="json")
  114. self.assertEqual(res.status_code, 429)
  115. def test_project_first_event(self):
  116. with open("events/test_data/py_error.json") as json_file:
  117. data = json.load(json_file)
  118. self.assertFalse(self.project.first_event)
  119. self.client.post(self.url, data, format="json")
  120. self.project.refresh_from_db()
  121. self.assertTrue(self.project.first_event)
  122. def test_null_character_event(self):
  123. """
  124. Unicode null characters \u0000 are not supported by Postgres JSONB
  125. NUL \x00 characters are not supported by Postgres string types
  126. They should be filtered out
  127. """
  128. with open("events/test_data/py_error.json") as json_file:
  129. data = json.load(json_file)
  130. data["exception"]["values"][0]["stacktrace"]["frames"][0][
  131. "function"
  132. ] = "a\u0000a"
  133. data["exception"]["values"][0]["value"] = "\x00\u0000"
  134. res = self.client.post(self.url, data, format="json")
  135. self.assertEqual(res.status_code, 200)
  136. def test_header_value_array(self):
  137. """
  138. Request Header values are both strings and arrays (sentry-php uses arrays)
  139. """
  140. with open("events/test_data/py_error.json") as json_file:
  141. data = json.load(json_file)
  142. data["request"]["headers"]["Content-Type"] = ["text/plain"]
  143. res = self.client.post(self.url, data, format="json")
  144. self.assertEqual(res.status_code, 200)
  145. event = Event.objects.first()
  146. header = next(
  147. x for x in event.data["request"]["headers"] if x[0] == "Content-Type"
  148. )
  149. self.assertTrue(isinstance(header[1], str))
  150. def test_anonymize_ip(self):
  151. """ip address should get masked because default project settings are to scrub ip address"""
  152. with open("events/test_data/py_hi_event.json") as json_file:
  153. data = json.load(json_file)
  154. test_ip = "123.168.29.14"
  155. res = self.client.post(self.url, data, format="json", REMOTE_ADDR=test_ip)
  156. self.assertEqual(res.status_code, 200)
  157. event = Event.objects.first()
  158. self.assertNotEqual(event.data["user"]["ip_address"], test_ip)
  159. def test_csp_event_anonymize_ip(self):
  160. url = reverse("csp_store", args=[self.project.id]) + self.params
  161. test_ip = "123.168.29.14"
  162. data = mdn_sample_csp
  163. res = self.client.post(url, data, format="json", REMOTE_ADDR=test_ip)
  164. self.assertEqual(res.status_code, 200)
  165. event = Event.objects.first()
  166. self.assertNotEqual(event.data["user"]["ip_address"], test_ip)
  167. def test_store_very_large_data(self):
  168. """
  169. This test is expected to exceed the 1mb limit of a postgres tsvector
  170. """
  171. with open("events/test_data/py_hi_event.json") as json_file:
  172. data = json.load(json_file)
  173. data["platform"] = " ".join([str(random.random()) for _ in range(50000)])
  174. res = self.client.post(self.url, data, format="json")
  175. self.assertEqual(res.status_code, 200)
  176. self.assertEqual(
  177. Issue.objects.first().search_vector,
  178. "",
  179. "No tsvector is expected as it would exceed the Postgres limit",
  180. )
  181. data["event_id"] = "6600a066e64b4caf8ed7ec5af64ac4be"
  182. res = self.client.post(self.url, data, format="json")
  183. self.assertEqual(res.status_code, 200)
  184. @patch("events.views.logger")
  185. def test_invalid_event(self, mock_logger):
  186. with open("events/test_data/py_hi_event.json") as json_file:
  187. data = json.load(json_file)
  188. data["transaction"] = True
  189. res = self.client.post(self.url, data, format="json")
  190. self.assertEqual(res.status_code, 200)
  191. mock_logger.warning.assert_called()
  192. def test_breadcrumbs_object(self):
  193. """Event breadcrumbs may be sent as an array or a object."""
  194. with open("events/test_data/py_hi_event.json") as json_file:
  195. data = json.load(json_file)
  196. data["breadcrumbs"] = {
  197. "values": [
  198. {
  199. "timestamp": "2020-01-20T20:00:00.000Z",
  200. "message": "Something",
  201. "category": "log",
  202. "data": {"foo": "bar"},
  203. },
  204. ]
  205. }
  206. res = self.client.post(self.url, data, format="json")
  207. self.assertEqual(res.status_code, 200)
  208. self.assertTrue(Issue.objects.exists())
  209. def test_event_release(self):
  210. with open("events/test_data/py_hi_event.json") as json_file:
  211. data = json.load(json_file)
  212. self.client.post(self.url, data, format="json")
  213. event = Event.objects.first()
  214. event_json = event.event_json()
  215. self.assertTrue(event.release)
  216. self.assertEqual(event_json.get("release"), event.release.version)
  217. self.assertIn(
  218. event.release.version,
  219. dict(event_json.get("tags")).values(),
  220. )
  221. def test_client_tags(self):
  222. with open("events/test_data/py_hi_event.json") as json_file:
  223. data = json.load(json_file)
  224. data["tags"] = {"test_tag": "the value"}
  225. self.client.post(self.url, data, format="json")
  226. event = Event.objects.first()
  227. event_json = event.event_json()
  228. self.assertIn(
  229. "the value",
  230. tuple(event_json.get("tags"))[1],
  231. )
  232. def test_client_tags_invalid(self):
  233. """Invalid tags should not be saved. But should not error."""
  234. with open("events/test_data/py_hi_event.json") as json_file:
  235. data = json.load(json_file)
  236. data["tags"] = {
  237. "value": "valid value",
  238. "my invalid tag key": {"oh": "this is invalid"},
  239. }
  240. res = self.client.post(self.url, data, format="json")
  241. event = Event.objects.first()
  242. self.assertEqual(res.status_code, 200)
  243. self.assertTrue(event)
  244. event_json = event.event_json()
  245. tags = tuple(event_json.get("tags"))
  246. self.assertIn(
  247. "valid value",
  248. tags[0],
  249. )
  250. for tag in tags:
  251. self.assertNotIn("this is invalid", tag)
  252. self.assertEqual(len(event_json.get("errors")), 1)
  253. def test_malformed_exception_value(self):
  254. """Malformed exception values aren't 100% supported, but should stored anyway"""
  255. with open("events/test_data/py_error.json") as json_file:
  256. data = json.load(json_file)
  257. data["exception"]["values"][0]["value"] = {"why is this": "any object?"}
  258. res = self.client.post(self.url, data, format="json")
  259. self.assertEqual(res.status_code, 200)
  260. def test_no_sdk(self):
  261. data = {
  262. "exception": [
  263. {
  264. "type": "Plug.Parsers.ParseError",
  265. "value": "malformed request",
  266. "module": None,
  267. }
  268. ],
  269. "culprit": "Plug.Parsers.JSON.decode",
  270. "extra": {},
  271. "event_id": "11111111111111111111111111111111",
  272. "breadcrumbs": [],
  273. "level": "error",
  274. "modules": {
  275. "cowboy": "2.8.0",
  276. },
  277. "fingerprint": ["{{ default }}"],
  278. "message": "(Plug.Parsers.ParseError) malformed",
  279. }
  280. res = self.client.post(self.url, data, format="json")
  281. self.assertEqual(res.status_code, 200)
  282. self.assertTrue(Event.objects.exists())
  283. def test_invalid_level(self):
  284. data = {
  285. "exception": [
  286. {
  287. "type": "a",
  288. "value": "a",
  289. "module": None,
  290. }
  291. ],
  292. "culprit": "a",
  293. "extra": {},
  294. "event_id": "11111111111111111111111111111111",
  295. "breadcrumbs": [],
  296. "level": "haha",
  297. "message": "a",
  298. }
  299. res = self.client.post(self.url, data, format="json")
  300. self.assertEqual(res.status_code, 200)
  301. self.assertTrue(Event.objects.filter(level=LogLevel.ERROR).exists())
  302. def test_null_release(self):
  303. data = {
  304. "exception": [
  305. {
  306. "type": "a",
  307. "value": "a",
  308. "module": None,
  309. }
  310. ],
  311. "culprit": "a",
  312. "extra": {},
  313. "event_id": "11111111111111111111111111111111",
  314. "breadcrumbs": [],
  315. "level": "haha",
  316. "message": "",
  317. "release": None,
  318. "environment": None,
  319. "request": {"env": {"FOO": None}},
  320. }
  321. res = self.client.post(self.url, data, format="json")
  322. self.assertEqual(res.status_code, 200)
  323. self.assertTrue(Event.objects.filter().exists())
  324. def test_formatted_message(self):
  325. data = {
  326. "exception": [
  327. {
  328. "type": "a",
  329. "value": "a",
  330. "module": None,
  331. }
  332. ],
  333. "event_id": "11111111111111111111111111111111",
  334. "message": {"formatted": "Hello"},
  335. }
  336. res = self.client.post(self.url, data, format="json")
  337. self.assertTrue(Event.objects.filter(data__message="Hello").exists())
  338. def test_invalid_message(self):
  339. # It's actually accepted as is. Considered to be message: ""
  340. data = {
  341. "exception": [
  342. {
  343. "type": "a",
  344. "value": "a",
  345. "module": None,
  346. }
  347. ],
  348. "event_id": "11111111111111111111111111111111",
  349. "message": {},
  350. }
  351. res = self.client.post(self.url, data, format="json")
  352. self.assertTrue(Event.objects.filter(data__message="").exists())
  353. def test_null_message(self):
  354. data = {
  355. "exception": [{}],
  356. "event_id": "11111111111111111111111111111111",
  357. "message": None,
  358. }
  359. res = self.client.post(self.url, data, format="json")
  360. self.assertTrue(Event.objects.filter(data__message=None).exists())
  361. def test_long_environment(self):
  362. data = {
  363. "exception": [
  364. {
  365. "type": "a",
  366. "value": "a",
  367. "module": None,
  368. }
  369. ],
  370. "event_id": "11111111111111111111111111111111",
  371. "environment": "a" * 257,
  372. }
  373. res = self.client.post(self.url, data, format="json")
  374. self.assertTrue(Event.objects.filter().exists())
  375. def test_invalid_environment(self):
  376. data = {
  377. "exception": [
  378. {
  379. "type": "a",
  380. "value": "a",
  381. "module": None,
  382. }
  383. ],
  384. "event_id": "11111111111111111111111111111111",
  385. "environment": "a/a",
  386. }
  387. res = self.client.post(self.url, data, format="json")
  388. self.assertTrue(Event.objects.filter().exists())
  389. self.assertFalse(Environment.objects.exists())
  390. def test_query_string_formats(self):
  391. data = {
  392. "event_id": "11111111111111111111111111111111",
  393. "exception": [
  394. {
  395. "type": "a",
  396. "value": "a",
  397. "module": None,
  398. }
  399. ],
  400. "request": {
  401. "method": "GET",
  402. "query_string": {"search": "foo"},
  403. },
  404. }
  405. self.client.post(self.url, data, format="json")
  406. data = {
  407. "event_id": "11111111111111111111111111111112",
  408. "exception": [
  409. {
  410. "type": "a",
  411. "value": "a",
  412. "module": None,
  413. }
  414. ],
  415. "request": {
  416. "query_string": "search=foo",
  417. },
  418. }
  419. self.client.post(self.url, data, format="json")
  420. data = {
  421. "event_id": "11111111111111111111111111111113",
  422. "exception": [
  423. {
  424. "type": "a",
  425. "value": "a",
  426. "module": None,
  427. }
  428. ],
  429. "request": {"query_string": [["search", "foo"]]},
  430. }
  431. self.client.post(self.url, data, format="json")
  432. self.assertEqual(
  433. Event.objects.filter(
  434. data__request__query_string=[["search", "foo"]]
  435. ).count(),
  436. 3,
  437. )