tests.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  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 six import StringIO
  19. from sentry.models import (Group, Event)
  20. from sentry.testutils import TestCase, TransactionTestCase
  21. from sentry.testutils.helpers import get_auth_header
  22. from sentry.utils.settings import (validate_settings, ConfigurationError, import_string)
  23. DEPENDENCY_TEST_DATA = {
  24. "postgresql": (
  25. 'DATABASES', 'psycopg2.extensions', "database engine",
  26. "django.db.backends.postgresql_psycopg2", {
  27. 'default': {
  28. 'ENGINE': "django.db.backends.postgresql_psycopg2",
  29. 'NAME': 'test',
  30. 'USER': 'root',
  31. 'PASSWORD': '',
  32. 'HOST': 'localhost',
  33. 'PORT': ''
  34. }
  35. }
  36. ),
  37. "mysql": (
  38. 'DATABASES', 'MySQLdb', "database engine", "django.db.backends.mysql", {
  39. 'default': {
  40. 'ENGINE': "django.db.backends.mysql",
  41. 'NAME': 'test',
  42. 'USER': 'root',
  43. 'PASSWORD': '',
  44. 'HOST': 'localhost',
  45. 'PORT': ''
  46. }
  47. }
  48. ),
  49. "oracle": (
  50. 'DATABASES', 'cx_Oracle', "database engine", "django.db.backends.oracle", {
  51. 'default': {
  52. 'ENGINE': "django.db.backends.oracle",
  53. 'NAME': 'test',
  54. 'USER': 'root',
  55. 'PASSWORD': '',
  56. 'HOST': 'localhost',
  57. 'PORT': ''
  58. }
  59. }
  60. ),
  61. "memcache": (
  62. 'CACHES', 'memcache', "caching backend",
  63. "django.core.cache.backends.memcached.MemcachedCache", {
  64. 'default': {
  65. 'BACKEND': "django.core.cache.backends.memcached.MemcachedCache",
  66. 'LOCATION': '127.0.0.1:11211',
  67. }
  68. }
  69. ),
  70. "pylibmc": (
  71. 'CACHES', 'pylibmc', "caching backend", "django.core.cache.backends.memcached.PyLibMCCache",
  72. {
  73. 'default': {
  74. 'BACKEND': "django.core.cache.backends.memcached.PyLibMCCache",
  75. 'LOCATION': '127.0.0.1:11211',
  76. }
  77. }
  78. ),
  79. }
  80. def get_fixture_path(name):
  81. return os.path.join(os.path.dirname(__file__), 'fixtures', name)
  82. def load_fixture(name):
  83. with open(get_fixture_path(name)) as fp:
  84. return fp.read()
  85. class RavenIntegrationTest(TransactionTestCase):
  86. """
  87. This mocks the test server and specifically tests behavior that would
  88. happen between Raven <--> Sentry over HTTP communication.
  89. """
  90. def setUp(self):
  91. self.user = self.create_user('coreapi@example.com')
  92. self.project = self.create_project()
  93. self.pk = self.project.key_set.get_or_create()[0]
  94. self.configure_sentry_errors()
  95. def configure_sentry_errors(self):
  96. # delay raising of assertion errors to make sure they do not get
  97. # swallowed again
  98. failures = []
  99. class AssertHandler(logging.Handler):
  100. def emit(self, entry):
  101. failures.append(entry)
  102. assert_handler = AssertHandler()
  103. for name in 'sentry.errors', 'sentry_sdk.errors':
  104. sentry_errors = logging.getLogger(name)
  105. sentry_errors.addHandler(assert_handler)
  106. sentry_errors.setLevel(logging.DEBUG)
  107. @self.addCleanup
  108. def remove_handler(sentry_errors=sentry_errors):
  109. sentry_errors.handlers.pop(sentry_errors.handlers.index(assert_handler))
  110. @self.addCleanup
  111. def reraise_failures():
  112. for entry in failures:
  113. raise AssertionError(entry.message)
  114. def send_event(self, method, url, body, headers):
  115. from sentry.app import buffer
  116. with self.tasks():
  117. content_type = headers.pop('Content-Type', None)
  118. headers = {'HTTP_' + k.replace('-', '_').upper(): v for k, v in six.iteritems(headers)}
  119. resp = self.client.post(
  120. reverse(
  121. 'sentry-api-store',
  122. args=[self.pk.project_id],
  123. ),
  124. data=body,
  125. content_type=content_type,
  126. **headers
  127. )
  128. assert resp.status_code == 200
  129. buffer.process_pending()
  130. @mock.patch('urllib3.PoolManager.request')
  131. def test_basic(self, request):
  132. requests = []
  133. def queue_event(method, url, body, headers):
  134. requests.append((method, url, body, headers))
  135. request.side_effect = queue_event
  136. hub = Hub(Client(
  137. 'http://%s:%s@localhost:8000/%s' %
  138. (self.pk.public_key, self.pk.secret_key, self.pk.project_id),
  139. default_integrations=False
  140. ))
  141. hub.capture_message('foo')
  142. hub.client.close()
  143. for _request in requests:
  144. self.send_event(*_request)
  145. assert request.call_count is 1
  146. assert Group.objects.count() == 1
  147. group = Group.objects.get()
  148. assert group.event_set.count() == 1
  149. instance = group.event_set.get()
  150. assert instance.data['logentry']['formatted'] == 'foo'
  151. class SentryRemoteTest(TestCase):
  152. @fixture
  153. def path(self):
  154. return reverse('sentry-api-store')
  155. def test_minimal(self):
  156. kwargs = {'message': 'hello', 'tags': {'foo': 'bar'}}
  157. resp = self._postWithHeader(kwargs)
  158. assert resp.status_code == 200, resp.content
  159. event_id = json.loads(resp.content)['id']
  160. instance = Event.objects.get(event_id=event_id)
  161. assert instance.message == 'hello'
  162. assert tagstore.get_tag_key(self.project.id, None, 'foo') is not None
  163. assert tagstore.get_tag_value(self.project.id, None, 'foo', 'bar') is not None
  164. assert tagstore.get_group_tag_key(
  165. self.project.id, instance.group_id, None, 'foo') is not None
  166. assert tagstore.get_group_tag_value(
  167. instance.project_id,
  168. instance.group_id,
  169. None,
  170. 'foo',
  171. 'bar') is not None
  172. def test_timestamp(self):
  173. timestamp = timezone.now().replace(
  174. microsecond=0, tzinfo=timezone.utc
  175. ) - datetime.timedelta(hours=1)
  176. kwargs = {u'message': 'hello', 'timestamp': float(timestamp.strftime('%s.%f'))}
  177. resp = self._postWithSignature(kwargs)
  178. assert resp.status_code == 200, resp.content
  179. instance = Event.objects.get()
  180. assert instance.message == 'hello'
  181. assert instance.datetime == timestamp
  182. group = instance.group
  183. assert group.first_seen == timestamp
  184. assert group.last_seen == timestamp
  185. def test_timestamp_as_iso(self):
  186. timestamp = timezone.now().replace(
  187. microsecond=0, tzinfo=timezone.utc
  188. ) - datetime.timedelta(hours=1)
  189. kwargs = {u'message': 'hello', 'timestamp': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%f')}
  190. resp = self._postWithSignature(kwargs)
  191. assert resp.status_code == 200, resp.content
  192. instance = Event.objects.get()
  193. assert instance.message == 'hello'
  194. assert instance.datetime == timestamp
  195. group = instance.group
  196. assert group.first_seen == timestamp
  197. assert group.last_seen == timestamp
  198. def test_ungzipped_data(self):
  199. kwargs = {'message': 'hello'}
  200. resp = self._postWithSignature(kwargs)
  201. assert resp.status_code == 200
  202. instance = Event.objects.get()
  203. assert instance.message == 'hello'
  204. @override_settings(SENTRY_ALLOW_ORIGIN='sentry.io')
  205. def test_correct_data_with_get(self):
  206. kwargs = {'message': 'hello'}
  207. resp = self._getWithReferer(kwargs)
  208. assert resp.status_code == 200, resp.content
  209. instance = Event.objects.get()
  210. assert instance.message == 'hello'
  211. @override_settings(SENTRY_ALLOW_ORIGIN='*')
  212. def test_get_without_referer_allowed(self):
  213. self.project.update_option('sentry:origins', '')
  214. kwargs = {'message': 'hello'}
  215. resp = self._getWithReferer(kwargs, referer=None, protocol='4')
  216. assert resp.status_code == 200, resp.content
  217. @override_settings(SENTRY_ALLOW_ORIGIN='sentry.io')
  218. def test_correct_data_with_post_referer(self):
  219. kwargs = {'message': 'hello'}
  220. resp = self._postWithReferer(kwargs)
  221. assert resp.status_code == 200, resp.content
  222. instance = Event.objects.get()
  223. assert instance.message == 'hello'
  224. @override_settings(SENTRY_ALLOW_ORIGIN='sentry.io')
  225. def test_post_without_referer(self):
  226. self.project.update_option('sentry:origins', '')
  227. kwargs = {'message': 'hello'}
  228. resp = self._postWithReferer(kwargs, referer=None, protocol='4')
  229. assert resp.status_code == 200, resp.content
  230. @override_settings(SENTRY_ALLOW_ORIGIN='*')
  231. def test_post_without_referer_allowed(self):
  232. self.project.update_option('sentry:origins', '')
  233. kwargs = {'message': 'hello'}
  234. resp = self._postWithReferer(kwargs, referer=None, protocol='4')
  235. assert resp.status_code == 200, resp.content
  236. @override_settings(SENTRY_ALLOW_ORIGIN='google.com')
  237. def test_post_with_invalid_origin(self):
  238. self.project.update_option('sentry:origins', 'sentry.io')
  239. kwargs = {'message': 'hello'}
  240. resp = self._postWithReferer(
  241. kwargs,
  242. referer='https://getsentry.net',
  243. protocol='4'
  244. )
  245. assert resp.status_code == 403, resp.content
  246. def test_signature(self):
  247. kwargs = {'message': 'hello'}
  248. resp = self._postWithSignature(kwargs)
  249. assert resp.status_code == 200, resp.content
  250. instance = Event.objects.get()
  251. assert instance.message == 'hello'
  252. def test_content_encoding_deflate(self):
  253. kwargs = {'message': 'hello'}
  254. message = zlib.compress(json.dumps(kwargs))
  255. key = self.projectkey.public_key
  256. secret = self.projectkey.secret_key
  257. with self.tasks():
  258. resp = self.client.post(
  259. self.path,
  260. message,
  261. content_type='application/octet-stream',
  262. HTTP_CONTENT_ENCODING='deflate',
  263. HTTP_X_SENTRY_AUTH=get_auth_header('_postWithHeader', key, secret),
  264. )
  265. assert resp.status_code == 200, resp.content
  266. event_id = json.loads(resp.content)['id']
  267. instance = Event.objects.get(event_id=event_id)
  268. assert instance.message == 'hello'
  269. def test_content_encoding_gzip(self):
  270. kwargs = {'message': 'hello'}
  271. message = json.dumps(kwargs)
  272. fp = StringIO()
  273. try:
  274. f = GzipFile(fileobj=fp, mode='w')
  275. f.write(message)
  276. finally:
  277. f.close()
  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. fp.getvalue(),
  284. content_type='application/octet-stream',
  285. HTTP_CONTENT_ENCODING='gzip',
  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 = Event.objects.get(event_id=event_id)
  291. assert instance.message == 'hello'
  292. def test_protocol_v2_0_without_secret_key(self):
  293. kwargs = {'message': 'hello'}
  294. resp = self._postWithHeader(
  295. data=kwargs,
  296. key=self.projectkey.public_key,
  297. protocol='2.0',
  298. )
  299. assert resp.status_code == 200, resp.content
  300. event_id = json.loads(resp.content)['id']
  301. instance = Event.objects.get(event_id=event_id)
  302. assert instance.message == 'hello'
  303. def test_protocol_v3(self):
  304. kwargs = {'message': 'hello'}
  305. resp = self._postWithHeader(
  306. data=kwargs,
  307. key=self.projectkey.public_key,
  308. secret=self.projectkey.secret_key,
  309. protocol='3',
  310. )
  311. assert resp.status_code == 200, resp.content
  312. event_id = json.loads(resp.content)['id']
  313. instance = Event.objects.get(event_id=event_id)
  314. assert instance.message == 'hello'
  315. def test_protocol_v4(self):
  316. kwargs = {'message': 'hello'}
  317. resp = self._postWithHeader(
  318. data=kwargs,
  319. key=self.projectkey.public_key,
  320. secret=self.projectkey.secret_key,
  321. protocol='4',
  322. )
  323. assert resp.status_code == 200, resp.content
  324. event_id = json.loads(resp.content)['id']
  325. instance = Event.objects.get(event_id=event_id)
  326. assert instance.message == 'hello'
  327. def test_protocol_v5(self):
  328. kwargs = {'message': 'hello'}
  329. resp = self._postWithHeader(
  330. data=kwargs,
  331. key=self.projectkey.public_key,
  332. secret=self.projectkey.secret_key,
  333. protocol='5',
  334. )
  335. assert resp.status_code == 200, resp.content
  336. event_id = json.loads(resp.content)['id']
  337. instance = Event.objects.get(event_id=event_id)
  338. assert instance.message == 'hello'
  339. def test_protocol_v6(self):
  340. kwargs = {'message': 'hello'}
  341. resp = self._postWithHeader(
  342. data=kwargs,
  343. key=self.projectkey.public_key,
  344. secret=self.projectkey.secret_key,
  345. protocol='6',
  346. )
  347. assert resp.status_code == 200, resp.content
  348. event_id = json.loads(resp.content)['id']
  349. instance = Event.objects.get(event_id=event_id)
  350. assert instance.message == 'hello'
  351. class DepdendencyTest(TestCase):
  352. def raise_import_error(self, package):
  353. def callable(package_name):
  354. if package_name != package:
  355. return import_string(package_name)
  356. raise ImportError("No module named %s" % (package, ))
  357. return callable
  358. @mock.patch('django.conf.settings', mock.Mock())
  359. @mock.patch('sentry.utils.settings.import_string')
  360. def validate_dependency(
  361. self, key, package, dependency_type, dependency, setting_value, import_string
  362. ):
  363. import_string.side_effect = self.raise_import_error(package)
  364. with self.settings(**{key: setting_value}):
  365. with self.assertRaises(ConfigurationError):
  366. validate_settings(settings)
  367. def test_validate_fails_on_postgres(self):
  368. self.validate_dependency(*DEPENDENCY_TEST_DATA['postgresql'])
  369. def test_validate_fails_on_mysql(self):
  370. self.validate_dependency(*DEPENDENCY_TEST_DATA['mysql'])
  371. def test_validate_fails_on_oracle(self):
  372. self.validate_dependency(*DEPENDENCY_TEST_DATA['oracle'])
  373. def test_validate_fails_on_memcache(self):
  374. self.validate_dependency(*DEPENDENCY_TEST_DATA['memcache'])
  375. def test_validate_fails_on_pylibmc(self):
  376. self.validate_dependency(*DEPENDENCY_TEST_DATA['pylibmc'])
  377. def get_fixtures(name):
  378. path = os.path.join(os.path.dirname(__file__), 'fixtures/csp', name)
  379. try:
  380. with open(path + '_input.json', 'rb') as fp1:
  381. input = fp1.read()
  382. except IOError:
  383. input = None
  384. try:
  385. with open(path + '_output.json', 'rb') as fp2:
  386. output = json.load(fp2)
  387. except IOError:
  388. output = None
  389. return input, output
  390. class CspReportTest(TestCase):
  391. def assertReportCreated(self, input, output):
  392. resp = self._postCspWithHeader(input)
  393. assert resp.status_code == 201, resp.content
  394. assert Event.objects.count() == 1
  395. e = Event.objects.all()[0]
  396. Event.objects.bind_nodes([e], 'data')
  397. assert output['message'] == e.data['logentry']['formatted']
  398. for key, value in six.iteritems(output['tags']):
  399. assert e.get_tag(key) == value
  400. for key, value in six.iteritems(output['data']):
  401. assert e.data[key] == value
  402. def assertReportRejected(self, input):
  403. resp = self._postCspWithHeader(input)
  404. assert resp.status_code in (400, 403), resp.content
  405. def test_chrome_blocked_asset(self):
  406. self.assertReportCreated(*get_fixtures('chrome_blocked_asset'))
  407. def test_firefox_missing_effective_uri(self):
  408. input, _ = get_fixtures('firefox_blocked_asset')
  409. self.assertReportRejected(input)