tests.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  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 six
  8. from time import sleep
  9. import zlib
  10. import pytest
  11. from sentry.utils.compat import mock
  12. from sentry import eventstore, tagstore
  13. from django.conf import settings
  14. from django.core.urlresolvers import reverse
  15. from django.test.utils import override_settings
  16. from django.utils import timezone
  17. from exam import fixture
  18. from gzip import GzipFile
  19. from sentry_sdk import Hub, Client
  20. from sentry_sdk.integrations.celery import CeleryIntegration
  21. from sentry_sdk.integrations.django import DjangoIntegration
  22. from six import StringIO
  23. from werkzeug.test import Client as WerkzeugClient
  24. from sentry.models import Group
  25. from sentry.testutils import SnubaTestCase, TestCase, TransactionTestCase
  26. from sentry.testutils.helpers import get_auth_header
  27. from sentry.testutils.helpers.datetime import iso_format, before_now
  28. from sentry.utils.settings import validate_settings, ConfigurationError, import_string
  29. from sentry.utils.sdk import configure_scope
  30. from sentry.web.api import disable_transaction_events
  31. from sentry.wsgi import application
  32. DEPENDENCY_TEST_DATA = {
  33. "postgresql": (
  34. "DATABASES",
  35. "psycopg2.extensions",
  36. "database engine",
  37. "django.db.backends.postgresql_psycopg2",
  38. {
  39. "default": {
  40. "ENGINE": "django.db.backends.postgresql_psycopg2",
  41. "NAME": "test",
  42. "USER": "root",
  43. "PASSWORD": "",
  44. "HOST": "127.0.0.1",
  45. "PORT": "",
  46. }
  47. },
  48. ),
  49. "memcache": (
  50. "CACHES",
  51. "memcache",
  52. "caching backend",
  53. "django.core.cache.backends.memcached.MemcachedCache",
  54. {
  55. "default": {
  56. "BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
  57. "LOCATION": "127.0.0.1:11211",
  58. }
  59. },
  60. ),
  61. "pylibmc": (
  62. "CACHES",
  63. "pylibmc",
  64. "caching backend",
  65. "django.core.cache.backends.memcached.PyLibMCCache",
  66. {
  67. "default": {
  68. "BACKEND": "django.core.cache.backends.memcached.PyLibMCCache",
  69. "LOCATION": "127.0.0.1:11211",
  70. }
  71. },
  72. ),
  73. }
  74. def get_fixture_path(name):
  75. return os.path.join(os.path.dirname(__file__), "fixtures", name)
  76. def load_fixture(name):
  77. with open(get_fixture_path(name)) as fp:
  78. return fp.read()
  79. @pytest.mark.obsolete("Remove, behaviour changed, new behaviour tested in Relay")
  80. class RavenIntegrationTest(TransactionTestCase):
  81. """
  82. This mocks the test server and specifically tests behavior that would
  83. happen between Raven <--> Sentry over HTTP communication.
  84. """
  85. def setUp(self):
  86. self.user = self.create_user("coreapi@example.com")
  87. self.project = self.create_project()
  88. self.pk = self.project.key_set.get_or_create()[0]
  89. self.configure_sentry_errors()
  90. def configure_sentry_errors(self):
  91. # delay raising of assertion errors to make sure they do not get
  92. # swallowed again
  93. failures = []
  94. class AssertHandler(logging.Handler):
  95. def emit(self, entry):
  96. failures.append(entry)
  97. assert_handler = AssertHandler()
  98. for name in "sentry.errors", "sentry_sdk.errors":
  99. sentry_errors = logging.getLogger(name)
  100. sentry_errors.addHandler(assert_handler)
  101. sentry_errors.setLevel(logging.DEBUG)
  102. @self.addCleanup
  103. def remove_handler(sentry_errors=sentry_errors):
  104. sentry_errors.handlers.pop(sentry_errors.handlers.index(assert_handler))
  105. @self.addCleanup
  106. def reraise_failures():
  107. for entry in failures:
  108. raise AssertionError(entry.message)
  109. def send_event(self, method, url, body, headers):
  110. from sentry.app import buffer
  111. with self.tasks():
  112. content_type = headers.pop("Content-Type", None)
  113. headers = {"HTTP_" + k.replace("-", "_").upper(): v for k, v in six.iteritems(headers)}
  114. resp = self.client.post(
  115. reverse("sentry-api-store", args=[self.pk.project_id]),
  116. data=body,
  117. content_type=content_type,
  118. **headers
  119. )
  120. assert resp.status_code == 200
  121. buffer.process_pending()
  122. @mock.patch("urllib3.PoolManager.request")
  123. def test_basic(self, request):
  124. requests = []
  125. def queue_event(method, url, body, headers):
  126. requests.append((method, url, body, headers))
  127. request.side_effect = queue_event
  128. hub = Hub(
  129. Client(
  130. "http://%s:%s@localhost:8000/%s"
  131. % (self.pk.public_key, self.pk.secret_key, self.pk.project_id),
  132. default_integrations=False,
  133. )
  134. )
  135. hub.capture_message("foo")
  136. hub.client.close()
  137. for _request in requests:
  138. self.send_event(*_request)
  139. assert request.call_count == 1
  140. assert Group.objects.count() == 1
  141. group = Group.objects.get()
  142. assert group.data["title"] == "foo"
  143. class SentryRemoteTest(TestCase):
  144. @fixture
  145. def path(self):
  146. return reverse("sentry-api-store")
  147. def get_event(self, event_id):
  148. instance = eventstore.get_event_by_id(self.project.id, event_id)
  149. return instance
  150. def test_minimal(self):
  151. event_data = {
  152. "message": "hello",
  153. "tags": {"foo": "bar"},
  154. "timestamp": iso_format(before_now(seconds=1)),
  155. }
  156. event = self.store_event(event_data, self.project.id)
  157. assert event is not None
  158. instance = self.get_event(event.event_id)
  159. assert instance.message == "hello"
  160. assert instance.data["logentry"] == {"formatted": "hello"}
  161. assert instance.title == instance.data["title"] == "hello"
  162. assert instance.location is instance.data.get("location", None) is None
  163. assert tagstore.get_tag_key(self.project.id, None, "foo") is not None
  164. assert tagstore.get_tag_value(self.project.id, None, "foo", "bar") is not None
  165. assert (
  166. tagstore.get_group_tag_key(self.project.id, instance.group_id, None, "foo") is not None
  167. )
  168. assert (
  169. tagstore.get_group_tag_value(instance.project_id, instance.group_id, None, "foo", "bar")
  170. is not None
  171. )
  172. def test_exception(self):
  173. event_data = {
  174. "exception": {
  175. "type": "ZeroDivisionError",
  176. "value": "cannot divide by zero",
  177. "stacktrace": {
  178. "frames": [
  179. {
  180. "filename": "utils.py",
  181. "in_app": False,
  182. "function": "raise_it",
  183. "module": "utils",
  184. },
  185. {
  186. "filename": "main.py",
  187. "in_app": True,
  188. "function": "fail_it",
  189. "module": "main",
  190. },
  191. ]
  192. },
  193. },
  194. "tags": {"foo": "bar"},
  195. "timestamp": iso_format(before_now(seconds=1)),
  196. }
  197. event = self.store_event(event_data, self.project.id)
  198. assert event is not None
  199. instance = self.get_event(event.event_id)
  200. assert len(instance.data["exception"]) == 1
  201. assert (
  202. instance.title == instance.data["title"] == "ZeroDivisionError: cannot divide by zero"
  203. )
  204. assert instance.location == instance.data["location"] == "main.py"
  205. assert instance.culprit == instance.data["culprit"] == "main in fail_it"
  206. assert tagstore.get_tag_key(self.project.id, None, "foo") is not None
  207. assert tagstore.get_tag_value(self.project.id, None, "foo", "bar") is not None
  208. assert (
  209. tagstore.get_group_tag_key(self.project.id, instance.group_id, None, "foo") is not None
  210. )
  211. assert (
  212. tagstore.get_group_tag_value(instance.project_id, instance.group_id, None, "foo", "bar")
  213. is not None
  214. )
  215. def test_timestamp(self):
  216. timestamp = timezone.now().replace(microsecond=0, tzinfo=timezone.utc) - datetime.timedelta(
  217. hours=1
  218. )
  219. event_data = {u"message": "hello", "timestamp": float(timestamp.strftime("%s.%f"))}
  220. event = self.store_event(event_data, self.project.id)
  221. assert event is not None
  222. instance = self.get_event(event.event_id)
  223. assert instance.message == "hello"
  224. assert instance.datetime == timestamp
  225. group = instance.group
  226. assert group.first_seen == timestamp
  227. assert group.last_seen == timestamp
  228. @pytest.mark.obsolete("Test in relay")
  229. @override_settings(SENTRY_ALLOW_ORIGIN="sentry.io")
  230. def test_correct_data_with_get(self):
  231. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  232. resp = self._getWithReferer(kwargs)
  233. assert resp.status_code == 200, resp.content
  234. event_id = resp["X-Sentry-ID"]
  235. instance = self.get_event(event_id)
  236. assert instance.message == "hello"
  237. @pytest.mark.obsolete("Test in relay")
  238. @override_settings(SENTRY_ALLOW_ORIGIN="*")
  239. def test_get_without_referer_allowed(self):
  240. self.project.update_option("sentry:origins", "")
  241. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  242. resp = self._getWithReferer(kwargs, referer=None, protocol="4")
  243. assert resp.status_code == 200, resp.content
  244. @pytest.mark.obsolete("Test in relay")
  245. @override_settings(SENTRY_ALLOW_ORIGIN="sentry.io")
  246. def test_correct_data_with_post_referer(self):
  247. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  248. resp = self._postWithReferer(kwargs)
  249. assert resp.status_code == 200, resp.content
  250. event_id = json.loads(resp.content)["id"]
  251. instance = self.get_event(event_id)
  252. assert instance.message == "hello"
  253. @pytest.mark.obsolete("Test in relay")
  254. @override_settings(SENTRY_ALLOW_ORIGIN="sentry.io")
  255. def test_post_without_referer(self):
  256. self.project.update_option("sentry:origins", "")
  257. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  258. resp = self._postWithReferer(kwargs, referer=None, protocol="4")
  259. assert resp.status_code == 200, resp.content
  260. @pytest.mark.obsolete("Test in relay")
  261. @override_settings(SENTRY_ALLOW_ORIGIN="*")
  262. def test_post_without_referer_allowed(self):
  263. self.project.update_option("sentry:origins", "")
  264. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  265. resp = self._postWithReferer(kwargs, referer=None, protocol="4")
  266. assert resp.status_code == 200, resp.content
  267. @pytest.mark.obsolete("Test in relay")
  268. @override_settings(SENTRY_ALLOW_ORIGIN="google.com")
  269. def test_post_with_invalid_origin(self):
  270. self.project.update_option("sentry:origins", "sentry.io")
  271. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  272. resp = self._postWithReferer(kwargs, referer="https://getsentry.net", protocol="4")
  273. assert resp.status_code == 403, resp.content
  274. @pytest.mark.obsolete("Test in relay")
  275. def test_content_encoding_deflate(self):
  276. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  277. message = zlib.compress(json.dumps(kwargs))
  278. key = self.projectkey.public_key
  279. secret = self.projectkey.secret_key
  280. with self.tasks():
  281. resp = self.client.post(
  282. self.path,
  283. message,
  284. content_type="application/octet-stream",
  285. HTTP_CONTENT_ENCODING="deflate",
  286. HTTP_X_SENTRY_AUTH=get_auth_header("_postWithHeader", key, secret),
  287. )
  288. assert resp.status_code == 200, resp.content
  289. event_id = json.loads(resp.content)["id"]
  290. instance = self.get_event(event_id)
  291. assert instance.message == "hello"
  292. @pytest.mark.obsolete("Test in relay")
  293. def test_content_encoding_gzip(self):
  294. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  295. message = json.dumps(kwargs)
  296. fp = StringIO()
  297. try:
  298. f = GzipFile(fileobj=fp, mode="w")
  299. f.write(message)
  300. finally:
  301. f.close()
  302. key = self.projectkey.public_key
  303. secret = self.projectkey.secret_key
  304. with self.tasks():
  305. resp = self.client.post(
  306. self.path,
  307. fp.getvalue(),
  308. content_type="application/octet-stream",
  309. HTTP_CONTENT_ENCODING="gzip",
  310. HTTP_X_SENTRY_AUTH=get_auth_header("_postWithHeader", key, secret),
  311. )
  312. assert resp.status_code == 200, resp.content
  313. event_id = json.loads(resp.content)["id"]
  314. instance = self.get_event(event_id)
  315. assert instance.message == "hello"
  316. @pytest.mark.obsolete("Test in relay")
  317. def test_protocol_v2_0_without_secret_key(self):
  318. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  319. resp = self._postWithHeader(data=kwargs, key=self.projectkey.public_key, protocol="2.0")
  320. assert resp.status_code == 200, resp.content
  321. event_id = json.loads(resp.content)["id"]
  322. instance = self.get_event(event_id)
  323. assert instance.message == "hello"
  324. @pytest.mark.obsolete("Test in relay")
  325. def test_protocol_v3(self):
  326. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  327. resp = self._postWithHeader(
  328. data=kwargs,
  329. key=self.projectkey.public_key,
  330. secret=self.projectkey.secret_key,
  331. protocol="3",
  332. )
  333. assert resp.status_code == 200, resp.content
  334. event_id = json.loads(resp.content)["id"]
  335. instance = self.get_event(event_id)
  336. assert instance.message == "hello"
  337. @pytest.mark.obsolete("Test in relay")
  338. def test_protocol_v4(self):
  339. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  340. resp = self._postWithHeader(
  341. data=kwargs,
  342. key=self.projectkey.public_key,
  343. secret=self.projectkey.secret_key,
  344. protocol="4",
  345. )
  346. assert resp.status_code == 200, resp.content
  347. event_id = json.loads(resp.content)["id"]
  348. instance = self.get_event(event_id)
  349. assert instance.message == "hello"
  350. @pytest.mark.obsolete("Test in relay")
  351. def test_protocol_v5(self):
  352. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  353. resp = self._postWithHeader(
  354. data=kwargs,
  355. key=self.projectkey.public_key,
  356. secret=self.projectkey.secret_key,
  357. protocol="5",
  358. )
  359. assert resp.status_code == 200, resp.content
  360. event_id = json.loads(resp.content)["id"]
  361. instance = self.get_event(event_id)
  362. assert instance.message == "hello"
  363. @pytest.mark.obsolete("Test in relay")
  364. def test_protocol_v6(self):
  365. kwargs = {"message": "hello", "timestamp": iso_format(before_now(seconds=1))}
  366. resp = self._postWithHeader(
  367. data=kwargs,
  368. key=self.projectkey.public_key,
  369. secret=self.projectkey.secret_key,
  370. protocol="6",
  371. )
  372. assert resp.status_code == 200, resp.content
  373. event_id = json.loads(resp.content)["id"]
  374. instance = self.get_event(event_id)
  375. assert instance.message == "hello"
  376. @pytest.mark.obsolete("Functionality not relevant in Relay store")
  377. class SentryWsgiRemoteTest(TransactionTestCase):
  378. @override_settings(ALLOWED_HOSTS=["localhost"])
  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, SnubaTestCase):
  464. def assertReportCreated(self, input, output):
  465. resp = self._postCspWithHeader(input)
  466. assert resp.status_code == 201, resp.content
  467. # XXX: there appears to be a race condition between the 201 return and get_events,
  468. # leading this test to sometimes fail. .5s seems to be sufficient.
  469. # Modifying the timestamp of store_event, like how it's done in other snuba tests,
  470. # doesn't work here because the event isn't created directly by this test.
  471. sleep(0.5)
  472. events = eventstore.get_events(
  473. filter=eventstore.Filter(
  474. project_ids=[self.project.id], conditions=[["type", "=", "csp"]]
  475. )
  476. )
  477. assert len(events) == 1
  478. e = events[0]
  479. assert output["message"] == e.data["logentry"]["formatted"]
  480. for key, value in six.iteritems(output["tags"]):
  481. assert e.get_tag(key) == value
  482. for key, value in six.iteritems(output["data"]):
  483. assert e.data[key] == value
  484. def assertReportRejected(self, input):
  485. resp = self._postCspWithHeader(input)
  486. assert resp.status_code in (400, 403), resp.content
  487. def test_invalid_report(self):
  488. self.assertReportRejected("")
  489. def test_chrome_blocked_asset(self):
  490. self.assertReportCreated(*get_fixtures("chrome_blocked_asset"))
  491. def test_firefox_missing_effective_uri(self):
  492. self.assertReportCreated(*get_fixtures("firefox_blocked_asset"))