tests.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. # -*- coding: utf-8 -*-
  2. from __future__ import absolute_import, print_function
  3. import os
  4. import datetime
  5. import json
  6. import logging
  7. import mock
  8. import six
  9. import zlib
  10. from sentry import tagstore
  11. from django.conf import settings
  12. from django.core.urlresolvers import reverse
  13. from django.test.utils import override_settings
  14. from django.utils import timezone
  15. from exam import fixture
  16. from gzip import GzipFile
  17. from sentry_sdk import Hub, Client
  18. from sentry_sdk.integrations.celery import CeleryIntegration
  19. from sentry_sdk.integrations.django import DjangoIntegration
  20. from six import StringIO
  21. from werkzeug.test import Client as WerkzeugClient
  22. from sentry.models import Group, Event
  23. from sentry.testutils import TestCase, TransactionTestCase
  24. from sentry.testutils.helpers import get_auth_header
  25. from sentry.utils.settings import validate_settings, ConfigurationError, import_string
  26. from sentry.utils.sdk import configure_scope
  27. from sentry.web.api import disable_transaction_events
  28. from sentry.wsgi import application
  29. DEPENDENCY_TEST_DATA = {
  30. "postgresql": (
  31. "DATABASES",
  32. "psycopg2.extensions",
  33. "database engine",
  34. "django.db.backends.postgresql_psycopg2",
  35. {
  36. "default": {
  37. "ENGINE": "django.db.backends.postgresql_psycopg2",
  38. "NAME": "test",
  39. "USER": "root",
  40. "PASSWORD": "",
  41. "HOST": "localhost",
  42. "PORT": "",
  43. }
  44. },
  45. ),
  46. "memcache": (
  47. "CACHES",
  48. "memcache",
  49. "caching backend",
  50. "django.core.cache.backends.memcached.MemcachedCache",
  51. {
  52. "default": {
  53. "BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
  54. "LOCATION": "127.0.0.1:11211",
  55. }
  56. },
  57. ),
  58. "pylibmc": (
  59. "CACHES",
  60. "pylibmc",
  61. "caching backend",
  62. "django.core.cache.backends.memcached.PyLibMCCache",
  63. {
  64. "default": {
  65. "BACKEND": "django.core.cache.backends.memcached.PyLibMCCache",
  66. "LOCATION": "127.0.0.1:11211",
  67. }
  68. },
  69. ),
  70. }
  71. def get_fixture_path(name):
  72. return os.path.join(os.path.dirname(__file__), "fixtures", name)
  73. def load_fixture(name):
  74. with open(get_fixture_path(name)) as fp:
  75. return fp.read()
  76. class RavenIntegrationTest(TransactionTestCase):
  77. """
  78. This mocks the test server and specifically tests behavior that would
  79. happen between Raven <--> Sentry over HTTP communication.
  80. """
  81. def setUp(self):
  82. self.user = self.create_user("coreapi@example.com")
  83. self.project = self.create_project()
  84. self.pk = self.project.key_set.get_or_create()[0]
  85. self.configure_sentry_errors()
  86. def configure_sentry_errors(self):
  87. # delay raising of assertion errors to make sure they do not get
  88. # swallowed again
  89. failures = []
  90. class AssertHandler(logging.Handler):
  91. def emit(self, entry):
  92. failures.append(entry)
  93. assert_handler = AssertHandler()
  94. for name in "sentry.errors", "sentry_sdk.errors":
  95. sentry_errors = logging.getLogger(name)
  96. sentry_errors.addHandler(assert_handler)
  97. sentry_errors.setLevel(logging.DEBUG)
  98. @self.addCleanup
  99. def remove_handler(sentry_errors=sentry_errors):
  100. sentry_errors.handlers.pop(sentry_errors.handlers.index(assert_handler))
  101. @self.addCleanup
  102. def reraise_failures():
  103. for entry in failures:
  104. raise AssertionError(entry.message)
  105. def send_event(self, method, url, body, headers):
  106. from sentry.app import buffer
  107. with self.tasks():
  108. content_type = headers.pop("Content-Type", None)
  109. headers = {"HTTP_" + k.replace("-", "_").upper(): v for k, v in six.iteritems(headers)}
  110. resp = self.client.post(
  111. reverse("sentry-api-store", args=[self.pk.project_id]),
  112. data=body,
  113. content_type=content_type,
  114. **headers
  115. )
  116. assert resp.status_code == 200
  117. buffer.process_pending()
  118. @mock.patch("urllib3.PoolManager.request")
  119. def test_basic(self, request):
  120. requests = []
  121. def queue_event(method, url, body, headers):
  122. requests.append((method, url, body, headers))
  123. request.side_effect = queue_event
  124. hub = Hub(
  125. Client(
  126. "http://%s:%s@localhost:8000/%s"
  127. % (self.pk.public_key, self.pk.secret_key, self.pk.project_id),
  128. default_integrations=False,
  129. )
  130. )
  131. hub.capture_message("foo")
  132. hub.client.close()
  133. for _request in requests:
  134. self.send_event(*_request)
  135. assert request.call_count is 1
  136. assert Group.objects.count() == 1
  137. group = Group.objects.get()
  138. assert group.data["title"] == "foo"
  139. class SentryRemoteTest(TestCase):
  140. @fixture
  141. def path(self):
  142. return reverse("sentry-api-store")
  143. def test_minimal(self):
  144. kwargs = {"message": "hello", "tags": {"foo": "bar"}}
  145. resp = self._postWithHeader(kwargs)
  146. assert resp.status_code == 200, resp.content
  147. event_id = json.loads(resp.content)["id"]
  148. instance = Event.objects.get(event_id=event_id)
  149. Event.objects.bind_nodes([instance], "data")
  150. assert instance.message == "hello"
  151. assert instance.data["logentry"] == {"formatted": "hello"}
  152. assert instance.title == instance.data["title"] == "hello"
  153. assert instance.location is instance.data.get("location", None) is None
  154. assert tagstore.get_tag_key(self.project.id, None, "foo") is not None
  155. assert tagstore.get_tag_value(self.project.id, None, "foo", "bar") is not None
  156. assert (
  157. tagstore.get_group_tag_key(self.project.id, instance.group_id, None, "foo") is not None
  158. )
  159. assert (
  160. tagstore.get_group_tag_value(instance.project_id, instance.group_id, None, "foo", "bar")
  161. is not None
  162. )
  163. def test_exception(self):
  164. kwargs = {
  165. "exception": {
  166. "type": "ZeroDivisionError",
  167. "value": "cannot divide by zero",
  168. "stacktrace": {
  169. "frames": [
  170. {
  171. "filename": "utils.py",
  172. "in_app": False,
  173. "function": "raise_it",
  174. "module": "utils",
  175. },
  176. {
  177. "filename": "main.py",
  178. "in_app": True,
  179. "function": "fail_it",
  180. "module": "main",
  181. },
  182. ]
  183. },
  184. },
  185. "tags": {"foo": "bar"},
  186. }
  187. resp = self._postWithHeader(kwargs)
  188. assert resp.status_code == 200, resp.content
  189. event_id = json.loads(resp.content)["id"]
  190. instance = Event.objects.get(event_id=event_id)
  191. Event.objects.bind_nodes([instance], "data")
  192. assert len(instance.data["exception"]) == 1
  193. assert (
  194. instance.title == instance.data["title"] == "ZeroDivisionError: cannot divide by zero"
  195. )
  196. assert instance.location == instance.data["location"] == "main.py"
  197. assert instance.culprit == instance.data["culprit"] == "main in fail_it"
  198. assert tagstore.get_tag_key(self.project.id, None, "foo") is not None
  199. assert tagstore.get_tag_value(self.project.id, None, "foo", "bar") is not None
  200. assert (
  201. tagstore.get_group_tag_key(self.project.id, instance.group_id, None, "foo") is not None
  202. )
  203. assert (
  204. tagstore.get_group_tag_value(instance.project_id, instance.group_id, None, "foo", "bar")
  205. is not None
  206. )
  207. def test_timestamp(self):
  208. timestamp = timezone.now().replace(microsecond=0, tzinfo=timezone.utc) - datetime.timedelta(
  209. hours=1
  210. )
  211. kwargs = {u"message": "hello", "timestamp": float(timestamp.strftime("%s.%f"))}
  212. resp = self._postWithSignature(kwargs)
  213. assert resp.status_code == 200, resp.content
  214. instance = Event.objects.get()
  215. assert instance.message == "hello"
  216. assert instance.datetime == timestamp
  217. group = instance.group
  218. assert group.first_seen == timestamp
  219. assert group.last_seen == timestamp
  220. def test_timestamp_as_iso(self):
  221. timestamp = timezone.now().replace(microsecond=0, tzinfo=timezone.utc) - datetime.timedelta(
  222. hours=1
  223. )
  224. kwargs = {u"message": "hello", "timestamp": timestamp.strftime("%Y-%m-%dT%H:%M:%S.%f")}
  225. resp = self._postWithSignature(kwargs)
  226. assert resp.status_code == 200, resp.content
  227. instance = Event.objects.get()
  228. assert instance.message == "hello"
  229. assert instance.datetime == timestamp
  230. group = instance.group
  231. assert group.first_seen == timestamp
  232. assert group.last_seen == timestamp
  233. def test_ungzipped_data(self):
  234. kwargs = {"message": "hello"}
  235. resp = self._postWithSignature(kwargs)
  236. assert resp.status_code == 200
  237. instance = Event.objects.get()
  238. assert instance.message == "hello"
  239. @override_settings(SENTRY_ALLOW_ORIGIN="sentry.io")
  240. def test_correct_data_with_get(self):
  241. kwargs = {"message": "hello"}
  242. resp = self._getWithReferer(kwargs)
  243. assert resp.status_code == 200, resp.content
  244. instance = Event.objects.get()
  245. assert instance.message == "hello"
  246. @override_settings(SENTRY_ALLOW_ORIGIN="*")
  247. def test_get_without_referer_allowed(self):
  248. self.project.update_option("sentry:origins", "")
  249. kwargs = {"message": "hello"}
  250. resp = self._getWithReferer(kwargs, referer=None, protocol="4")
  251. assert resp.status_code == 200, resp.content
  252. @override_settings(SENTRY_ALLOW_ORIGIN="sentry.io")
  253. def test_correct_data_with_post_referer(self):
  254. kwargs = {"message": "hello"}
  255. resp = self._postWithReferer(kwargs)
  256. assert resp.status_code == 200, resp.content
  257. instance = Event.objects.get()
  258. assert instance.message == "hello"
  259. @override_settings(SENTRY_ALLOW_ORIGIN="sentry.io")
  260. def test_post_without_referer(self):
  261. self.project.update_option("sentry:origins", "")
  262. kwargs = {"message": "hello"}
  263. resp = self._postWithReferer(kwargs, referer=None, protocol="4")
  264. assert resp.status_code == 200, resp.content
  265. @override_settings(SENTRY_ALLOW_ORIGIN="*")
  266. def test_post_without_referer_allowed(self):
  267. self.project.update_option("sentry:origins", "")
  268. kwargs = {"message": "hello"}
  269. resp = self._postWithReferer(kwargs, referer=None, protocol="4")
  270. assert resp.status_code == 200, resp.content
  271. @override_settings(SENTRY_ALLOW_ORIGIN="google.com")
  272. def test_post_with_invalid_origin(self):
  273. self.project.update_option("sentry:origins", "sentry.io")
  274. kwargs = {"message": "hello"}
  275. resp = self._postWithReferer(kwargs, referer="https://getsentry.net", protocol="4")
  276. assert resp.status_code == 403, resp.content
  277. def test_signature(self):
  278. kwargs = {"message": "hello"}
  279. resp = self._postWithSignature(kwargs)
  280. assert resp.status_code == 200, resp.content
  281. instance = Event.objects.get()
  282. assert instance.message == "hello"
  283. def test_content_encoding_deflate(self):
  284. kwargs = {"message": "hello"}
  285. message = zlib.compress(json.dumps(kwargs))
  286. key = self.projectkey.public_key
  287. secret = self.projectkey.secret_key
  288. with self.tasks():
  289. resp = self.client.post(
  290. self.path,
  291. message,
  292. content_type="application/octet-stream",
  293. HTTP_CONTENT_ENCODING="deflate",
  294. HTTP_X_SENTRY_AUTH=get_auth_header("_postWithHeader", key, secret),
  295. )
  296. assert resp.status_code == 200, resp.content
  297. event_id = json.loads(resp.content)["id"]
  298. instance = Event.objects.get(event_id=event_id)
  299. assert instance.message == "hello"
  300. def test_content_encoding_gzip(self):
  301. kwargs = {"message": "hello"}
  302. message = json.dumps(kwargs)
  303. fp = StringIO()
  304. try:
  305. f = GzipFile(fileobj=fp, mode="w")
  306. f.write(message)
  307. finally:
  308. f.close()
  309. key = self.projectkey.public_key
  310. secret = self.projectkey.secret_key
  311. with self.tasks():
  312. resp = self.client.post(
  313. self.path,
  314. fp.getvalue(),
  315. content_type="application/octet-stream",
  316. HTTP_CONTENT_ENCODING="gzip",
  317. HTTP_X_SENTRY_AUTH=get_auth_header("_postWithHeader", key, secret),
  318. )
  319. assert resp.status_code == 200, resp.content
  320. event_id = json.loads(resp.content)["id"]
  321. instance = Event.objects.get(event_id=event_id)
  322. assert instance.message == "hello"
  323. def test_protocol_v2_0_without_secret_key(self):
  324. kwargs = {"message": "hello"}
  325. resp = self._postWithHeader(data=kwargs, key=self.projectkey.public_key, protocol="2.0")
  326. assert resp.status_code == 200, resp.content
  327. event_id = json.loads(resp.content)["id"]
  328. instance = Event.objects.get(event_id=event_id)
  329. assert instance.message == "hello"
  330. def test_protocol_v3(self):
  331. kwargs = {"message": "hello"}
  332. resp = self._postWithHeader(
  333. data=kwargs,
  334. key=self.projectkey.public_key,
  335. secret=self.projectkey.secret_key,
  336. protocol="3",
  337. )
  338. assert resp.status_code == 200, resp.content
  339. event_id = json.loads(resp.content)["id"]
  340. instance = Event.objects.get(event_id=event_id)
  341. assert instance.message == "hello"
  342. def test_protocol_v4(self):
  343. kwargs = {"message": "hello"}
  344. resp = self._postWithHeader(
  345. data=kwargs,
  346. key=self.projectkey.public_key,
  347. secret=self.projectkey.secret_key,
  348. protocol="4",
  349. )
  350. assert resp.status_code == 200, resp.content
  351. event_id = json.loads(resp.content)["id"]
  352. instance = Event.objects.get(event_id=event_id)
  353. assert instance.message == "hello"
  354. def test_protocol_v5(self):
  355. kwargs = {"message": "hello"}
  356. resp = self._postWithHeader(
  357. data=kwargs,
  358. key=self.projectkey.public_key,
  359. secret=self.projectkey.secret_key,
  360. protocol="5",
  361. )
  362. assert resp.status_code == 200, resp.content
  363. event_id = json.loads(resp.content)["id"]
  364. instance = Event.objects.get(event_id=event_id)
  365. assert instance.message == "hello"
  366. def test_protocol_v6(self):
  367. kwargs = {"message": "hello"}
  368. resp = self._postWithHeader(
  369. data=kwargs,
  370. key=self.projectkey.public_key,
  371. secret=self.projectkey.secret_key,
  372. protocol="6",
  373. )
  374. assert resp.status_code == 200, resp.content
  375. event_id = json.loads(resp.content)["id"]
  376. instance = Event.objects.get(event_id=event_id)
  377. assert instance.message == "hello"
  378. class SentryWsgiRemoteTest(TransactionTestCase):
  379. def test_traceparent_header_wsgi(self):
  380. # Assert that posting something to store will not create another
  381. # (transaction) event under any circumstances.
  382. #
  383. # We use Werkzeug's test client because Django's test client bypasses a
  384. # lot of request handling code that we want to test implicitly (such as
  385. # all our WSGI middlewares and the entire Django instrumentation by
  386. # sentry-sdk).
  387. #
  388. # XXX(markus): Ideally methods such as `_postWithHeader` would always
  389. # call the WSGI application => swap out Django's test client with e.g.
  390. # Werkzeug's.
  391. client = WerkzeugClient(application)
  392. calls = []
  393. def new_disable_transaction_events():
  394. with configure_scope() as scope:
  395. assert scope.span.sampled
  396. assert scope.span.transaction
  397. disable_transaction_events()
  398. assert not scope.span.sampled
  399. calls.append(1)
  400. events = []
  401. auth = get_auth_header(
  402. "_postWithWerkzeug/0.0.0", self.projectkey.public_key, self.projectkey.secret_key, "7"
  403. )
  404. with mock.patch(
  405. "sentry.web.api.disable_transaction_events", new_disable_transaction_events
  406. ):
  407. with self.tasks():
  408. with Hub(
  409. Client(
  410. transport=events.append,
  411. integrations=[CeleryIntegration(), DjangoIntegration()],
  412. )
  413. ):
  414. app_iter, status, headers = client.post(
  415. reverse("sentry-api-store"),
  416. data=b'{"message": "hello"}',
  417. headers={
  418. "x-sentry-auth": auth,
  419. "sentry-trace": "1",
  420. "content-type": "application/octet-stream",
  421. },
  422. environ_base={"REMOTE_ADDR": "127.0.0.1"},
  423. )
  424. body = "".join(app_iter)
  425. assert status == "200 OK", body
  426. assert not events
  427. assert calls == [1]
  428. class DependencyTest(TestCase):
  429. def raise_import_error(self, package):
  430. def callable(package_name):
  431. if package_name != package:
  432. return import_string(package_name)
  433. raise ImportError("No module named %s" % (package,))
  434. return callable
  435. @mock.patch("django.conf.settings", mock.Mock())
  436. @mock.patch("sentry.utils.settings.import_string")
  437. def validate_dependency(
  438. self, key, package, dependency_type, dependency, setting_value, import_string
  439. ):
  440. import_string.side_effect = self.raise_import_error(package)
  441. with self.settings(**{key: setting_value}):
  442. with self.assertRaises(ConfigurationError):
  443. validate_settings(settings)
  444. def test_validate_fails_on_postgres(self):
  445. self.validate_dependency(*DEPENDENCY_TEST_DATA["postgresql"])
  446. def test_validate_fails_on_memcache(self):
  447. self.validate_dependency(*DEPENDENCY_TEST_DATA["memcache"])
  448. def test_validate_fails_on_pylibmc(self):
  449. self.validate_dependency(*DEPENDENCY_TEST_DATA["pylibmc"])
  450. def get_fixtures(name):
  451. path = os.path.join(os.path.dirname(__file__), "fixtures/csp", name)
  452. try:
  453. with open(path + "_input.json", "rb") as fp1:
  454. input = fp1.read()
  455. except IOError:
  456. input = None
  457. try:
  458. with open(path + "_output.json", "rb") as fp2:
  459. output = json.load(fp2)
  460. except IOError:
  461. output = None
  462. return input, output
  463. class CspReportTest(TestCase):
  464. def assertReportCreated(self, input, output):
  465. resp = self._postCspWithHeader(input)
  466. assert resp.status_code == 201, resp.content
  467. assert Event.objects.count() == 1
  468. e = Event.objects.all()[0]
  469. Event.objects.bind_nodes([e], "data")
  470. assert output["message"] == e.data["logentry"]["formatted"]
  471. for key, value in six.iteritems(output["tags"]):
  472. assert e.get_tag(key) == value
  473. for key, value in six.iteritems(output["data"]):
  474. assert e.data[key] == value
  475. def assertReportRejected(self, input):
  476. resp = self._postCspWithHeader(input)
  477. assert resp.status_code in (400, 403), resp.content
  478. def test_invalid_report(self):
  479. self.assertReportRejected("")
  480. def test_chrome_blocked_asset(self):
  481. self.assertReportCreated(*get_fixtures("chrome_blocked_asset"))
  482. def test_firefox_missing_effective_uri(self):
  483. self.assertReportCreated(*get_fixtures("firefox_blocked_asset"))