tests.py 20 KB

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