tests.py 16 KB

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