tests_dispatch.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2013-2016 The Distro Tracker Developers
  3. # See the COPYRIGHT file at the top-level directory of this distribution and
  4. # at http://deb.li/DTAuthors
  5. #
  6. # This file is part of Distro Tracker. It is subject to the license terms
  7. # in the LICENSE file found in the top-level directory of this
  8. # distribution and at http://deb.li/DTLicense. No part of Distro Tracker,
  9. # including this file, may be copied, modified, propagated, or distributed
  10. # except according to the terms contained in the LICENSE file.
  11. """
  12. This module contains the tests for the dispatch functionality
  13. (:py:mod:`distro_tracker.mail.dispatch` module) of distro-tracker.
  14. """
  15. from __future__ import unicode_literals
  16. from email.message import Message
  17. from datetime import timedelta
  18. import logging
  19. from django.core import mail
  20. from django.conf import settings
  21. from django.utils import timezone
  22. from django.utils.six.moves import mock
  23. from distro_tracker.accounts.models import UserEmail
  24. from distro_tracker.accounts.models import User
  25. from distro_tracker.core.models import PackageName, Subscription, Keyword
  26. from distro_tracker.core.models import Team
  27. from distro_tracker.core.utils import verp
  28. from distro_tracker.core.utils import get_decoded_message_payload
  29. from distro_tracker.core.utils import distro_tracker_render_to_string
  30. from distro_tracker.core.utils.email_messages import (
  31. patch_message_for_django_compat)
  32. from distro_tracker.mail import dispatch
  33. from distro_tracker.mail.models import UserEmailBounceStats
  34. from distro_tracker.test import TestCase
  35. DISTRO_TRACKER_CONTROL_EMAIL = settings.DISTRO_TRACKER_CONTROL_EMAIL
  36. DISTRO_TRACKER_FQDN = settings.DISTRO_TRACKER_FQDN
  37. logging.disable(logging.CRITICAL)
  38. class DispatchTestHelperMixin(object):
  39. """
  40. A mixin containing methods to assist testing dispatch functionality.
  41. """
  42. def clear_message(self):
  43. """
  44. Clears the test message being built.
  45. """
  46. self.message = Message()
  47. patch_message_for_django_compat(self.message)
  48. self.headers = []
  49. def set_package_name(self, package_name):
  50. """
  51. Sets the name of the test package.
  52. :param package_name: The new name of the test package
  53. """
  54. self.package_name = package_name
  55. self.add_header('To', '{package}@{distro_tracker_fqdn}'.format(
  56. package=self.package_name,
  57. distro_tracker_fqdn=DISTRO_TRACKER_FQDN))
  58. def set_message_content(self, content):
  59. """
  60. Sets the content of the test message.
  61. :param content: New content
  62. """
  63. self.message.set_payload(content)
  64. def add_header(self, header_name, header_value):
  65. """
  66. Adds a header to the test message.
  67. :param header_name: The name of the header which is to be added
  68. :param header_value: The value of the header which is to be added
  69. """
  70. self.message.add_header(header_name, header_value)
  71. self.headers.append((header_name, header_value))
  72. def set_header(self, header_name, header_value):
  73. """
  74. Sets a header of the test message to the given value.
  75. If the header previously existed in the message, it is overwritten.
  76. :param header_name: The name of the header to be set
  77. :param header_value: The new value of the header to be set.
  78. """
  79. if header_name in self.message:
  80. del self.message[header_name]
  81. self.add_header(header_name, header_value)
  82. def run_dispatch(self, package=None, keyword=None):
  83. """
  84. Starts the dispatch process.
  85. """
  86. dispatch.process(
  87. self.message,
  88. package=package or self.package_name,
  89. keyword=keyword,
  90. )
  91. def run_forward(self, package=None, keyword=None):
  92. """
  93. Starts the forward process.
  94. """
  95. dispatch.forward(
  96. self.message,
  97. package=package or self.package_name,
  98. keyword=keyword or "default",
  99. )
  100. def subscribe_user_with_keyword(self, email, keyword):
  101. """
  102. Creates a user subscribed to the package with the given keyword.
  103. """
  104. subscription = Subscription.objects.create_for(
  105. email=email,
  106. package_name=self.package.name
  107. )
  108. subscription.keywords.add(Keyword.objects.get(name=keyword))
  109. def subscribe_user_to_package(self, user_email, package, active=True):
  110. """
  111. Helper method which subscribes the given user to the given package.
  112. """
  113. Subscription.objects.create_for(
  114. package_name=package,
  115. email=user_email,
  116. active=active)
  117. def assert_message_forwarded_to(self, email):
  118. """
  119. Asserts that the message was forwarded to the given email.
  120. """
  121. self.assertTrue(mail.outbox)
  122. self.assertIn(email, (message.to[0] for message in mail.outbox))
  123. def assert_forward_content_equal(self, content):
  124. """
  125. Asserts that the content of the forwarded message is equal to the given
  126. ``content``.
  127. """
  128. msg = mail.outbox[0].message()
  129. self.assertEqual(get_decoded_message_payload(msg), content)
  130. def assert_all_headers_found(self, headers):
  131. """
  132. Asserts that all the given headers are found in the forwarded messages.
  133. """
  134. for msg in mail.outbox:
  135. msg = msg.message()
  136. for header_name, header_value in headers:
  137. self.assertIn(header_name, msg)
  138. self.assertIn(
  139. header_value, msg.get_all(header_name),
  140. '{header_name}: {header_value} not found in {all}'.format(
  141. header_name=header_name,
  142. header_value=header_value,
  143. all=msg.get_all(header_name)))
  144. def assert_header_equal(self, header_name, header_value):
  145. """
  146. Asserts that the header's value is equal to the given value.
  147. """
  148. # Ensure we have some messages to check against
  149. self.assertTrue(len(mail.outbox) > 0)
  150. for msg in mail.outbox:
  151. msg = msg.message()
  152. self.assertEqual(msg[header_name], header_value)
  153. class DispatchBaseTest(TestCase, DispatchTestHelperMixin):
  154. def setUp(self):
  155. self.clear_message()
  156. self.from_email = 'dummy-email@domain.com'
  157. self.set_package_name('dummy-package')
  158. self.add_header('From', 'Real Name <{from_email}>'.format(
  159. from_email=self.from_email))
  160. self.add_header('Subject', 'Some subject')
  161. self.add_header('X-Loop', 'owner@bugs.debian.org')
  162. self.add_header('X-Distro-Tracker-Approved', '1')
  163. self.set_message_content('message content')
  164. self.package = PackageName.objects.create(name=self.package_name)
  165. def test_forward_mail_serialize_to_bytes(self):
  166. """
  167. Tests that the message instance to be sent to subscribers can be
  168. serialized to bytes with no errors when the body contains utf-8
  169. """
  170. self.subscribe_user_to_package('user@domain.com', self.package_name)
  171. self.set_message_content('üößšđžčć한글')
  172. self.message.set_charset('utf-8')
  173. self.run_forward()
  174. msg = mail.outbox[0]
  175. # No exception thrown trying to get the entire message as bytes
  176. content = msg.message().as_string()
  177. # self.assertIs(msg.message(), self.message)
  178. # The content is actually bytes
  179. self.assertIsInstance(content, bytes)
  180. def test_forward_to_subscribers(self):
  181. """
  182. Tests the forward functionality when there users subscribed to it.
  183. """
  184. self.subscribe_user_to_package('user@domain.com', self.package_name)
  185. self.subscribe_user_to_package('user2@domain.com', self.package_name)
  186. self.run_forward()
  187. self.assert_message_forwarded_to('user@domain.com')
  188. self.assert_message_forwarded_to('user2@domain.com')
  189. def test_forward_all_old_headers(self):
  190. """
  191. Tests the forward functionality to check if all old headers are found
  192. in the forwarded message in the correct order.
  193. """
  194. self.subscribe_user_to_package('user@domain.com', self.package_name)
  195. self.run_forward()
  196. for old_header, fwd_header in zip(self.message.items(),
  197. mail.outbox[0].message().items()):
  198. self.assertEqual(old_header, fwd_header)
  199. def test_envelope_from_address(self):
  200. """
  201. Tests that the envelope from address is created specially for each user
  202. in order to track their bounced messages.
  203. """
  204. self.subscribe_user_to_package('user@domain.com', self.package_name)
  205. self.run_forward()
  206. msg = mail.outbox[0]
  207. bounce_address, user_address = verp.decode(msg.from_email)
  208. self.assertTrue(bounce_address.startswith('bounces+'))
  209. self.assertEqual(user_address, msg.to[0])
  210. def test_correct_foward_content(self):
  211. """
  212. Tests that the content of the forwarded message is unchanged.
  213. """
  214. self.subscribe_user_to_package('user@domain.com', self.package_name)
  215. original = 'Content of the message'
  216. self.set_message_content(original)
  217. self.run_forward()
  218. self.assert_forward_content_equal(original)
  219. def test_forward_all_new_headers(self):
  220. """
  221. Tests the forward functionality to check if all required new headers
  222. are found in the forwarded message.
  223. """
  224. headers = [
  225. ('X-Loop', 'dispatch@{}'.format(DISTRO_TRACKER_FQDN)),
  226. ('X-Distro-Tracker-Package', self.package_name),
  227. ('X-Distro-Tracker-Keyword', 'default'),
  228. ('Precedence', 'list'),
  229. ('List-Id', '<{}.{}>'.format(self.package_name,
  230. DISTRO_TRACKER_FQDN)),
  231. ('List-Unsubscribe',
  232. '<mailto:{control_email}?body=unsubscribe%20{package}>'.format(
  233. control_email=DISTRO_TRACKER_CONTROL_EMAIL,
  234. package=self.package_name)),
  235. ]
  236. self.subscribe_user_to_package('user@domain.com', self.package_name)
  237. self.run_forward()
  238. self.assert_all_headers_found(headers)
  239. def test_forward_package_doesnt_exist(self):
  240. """
  241. Tests the forward functionality when the given package does not
  242. exist.
  243. """
  244. self.set_package_name('non-existent-package')
  245. self.run_forward()
  246. self.assertEqual(len(mail.outbox), 0)
  247. def test_forward_package_no_subscribers(self):
  248. """
  249. Tests the forward functionality when the given package does not have
  250. any subscribers.
  251. """
  252. self.run_forward()
  253. self.assertEqual(len(mail.outbox), 0)
  254. def test_forward_inactive_subscription(self):
  255. """
  256. Tests the forward functionality when the subscriber's subscription
  257. is inactive.
  258. """
  259. self.subscribe_user_to_package('user@domain.com', self.package_name,
  260. active=False)
  261. self.run_forward()
  262. self.assertEqual(len(mail.outbox), 0)
  263. def test_utf8_message_forward(self):
  264. """
  265. Tests that a message is properly forwarded if it was utf-8 encoded.
  266. """
  267. self.subscribe_user_to_package('user@domain.com', self.package_name)
  268. self.set_message_content('üößšđžčć한글')
  269. self.message.set_charset('utf-8')
  270. self.run_forward()
  271. self.assert_forward_content_equal('üößšđžčć한글')
  272. def test_forwarded_mail_recorded(self):
  273. """
  274. Tests that when a mail is forwarded it is logged in the user's bounce
  275. information structure.
  276. """
  277. self.subscribe_user_to_package('user@domain.com', self.package_name)
  278. user = UserEmailBounceStats.objects.get(email='user@domain.com')
  279. self.run_forward()
  280. bounce_stats = user.bouncestats_set.all()
  281. self.assertEqual(bounce_stats.count(), 1)
  282. self.assertEqual(bounce_stats[0].date, timezone.now().date())
  283. self.assertEqual(bounce_stats[0].mails_sent, 1)
  284. def test_xloop_already_set(self):
  285. """
  286. Tests that the message is dropped when the X-Loop header is already
  287. set.
  288. """
  289. self.set_header('X-Loop', 'somevalue')
  290. self.set_header('X-Loop', 'dispatch@' + DISTRO_TRACKER_FQDN)
  291. self.subscribe_user_to_package('user@domain.com', self.package_name)
  292. self.run_forward()
  293. self.assertEqual(len(mail.outbox), 0)
  294. def test_forward_keyword_in_address(self):
  295. """
  296. Tests the forward functionality when the keyword of the message is
  297. given in the address the message was sent to (srcpackage_keyword)
  298. """
  299. self.subscribe_user_with_keyword('user@domain.com', 'vcs')
  300. self.run_forward(keyword='vcs')
  301. self.assert_message_forwarded_to('user@domain.com')
  302. self.assert_header_equal('X-Distro-Tracker-Keyword', 'vcs')
  303. def test_unknown_keyword(self):
  304. self.subscribe_user_to_package('user@domain.com', self.package_name)
  305. self.run_forward(keyword='unknown')
  306. self.assertEqual(len(mail.outbox), 0)
  307. def patch_forward(self):
  308. patcher = mock.patch('distro_tracker.mail.dispatch.forward')
  309. mocked = patcher.start()
  310. self.addCleanup(patcher.stop)
  311. return mocked
  312. def test_dispatch_calls_forward(self):
  313. mock_forward = self.patch_forward()
  314. self.run_dispatch('foo', 'bts')
  315. mock_forward.assert_called_with(self.message, 'foo', 'bts')
  316. def test_dispatch_does_not_call_forward_when_package_not_identified(self):
  317. mock_forward = self.patch_forward()
  318. self.package_name = None
  319. self.run_dispatch(None, None)
  320. self.assertFalse(mock_forward.called)
  321. @mock.patch('distro_tracker.mail.dispatch.classify_message')
  322. def test_dispatch_does_not_call_forward_when_classify_raises_exception(
  323. self, mock_classify):
  324. mock_forward = self.patch_forward()
  325. mock_classify.side_effect = dispatch.SkipMessage
  326. self.run_dispatch('foo', 'bts')
  327. self.assertFalse(mock_forward.called)
  328. def test_dispatch_calls_forward_with_multiple_packages(self):
  329. mock_forward = self.patch_forward()
  330. self.run_dispatch(['foo', 'bar', 'baz'], 'bts')
  331. mock_forward.assert_has_calls([
  332. mock.call(self.message, 'foo', 'bts'),
  333. mock.call(self.message, 'bar', 'bts'),
  334. mock.call(self.message, 'baz', 'bts')
  335. ])
  336. class ClassifyMessageTests(TestCase):
  337. def setUp(self):
  338. self.message = Message()
  339. def run_classify(self, package=None, keyword=None):
  340. return dispatch.classify_message(self.message, package=package,
  341. keyword=keyword)
  342. def patch_vendor_call(self, return_value=None):
  343. patcher = mock.patch('distro_tracker.vendor.call')
  344. mocked = patcher.start()
  345. mocked.return_value = (return_value, return_value is not None)
  346. self.addCleanup(patcher.stop)
  347. return mocked
  348. def test_classify_calls_vendor_classify_message(self):
  349. mock_vendor_call = self.patch_vendor_call()
  350. self.run_classify()
  351. mock_vendor_call.assert_called_with('classify_message', self.message,
  352. package=None, keyword=None)
  353. def test_classify_returns_default_values_without_vendor_classify(self):
  354. self.patch_vendor_call()
  355. package, keyword = self.run_classify(package='abc', keyword='vcs')
  356. self.assertEqual(package, 'abc')
  357. self.assertEqual(keyword, 'vcs')
  358. def test_classify_return_vendor_values_when_available(self):
  359. self.patch_vendor_call(('vendorpkg', 'bugs'))
  360. package, keyword = self.run_classify(package='abc', keyword='vcs')
  361. self.assertEqual(package, 'vendorpkg')
  362. self.assertEqual(keyword, 'bugs')
  363. def test_classify_uses_default_keyword_when_unknown(self):
  364. self.patch_vendor_call(('vendorpkg', None))
  365. package, keyword = self.run_classify()
  366. self.assertEqual(package, 'vendorpkg')
  367. self.assertEqual(keyword, 'default')
  368. def test_classify_uses_values_supplied_in_headers(self):
  369. self.message['X-Distro-Tracker-Package'] = 'headerpkg'
  370. self.message['X-Distro-Tracker-Keyword'] = 'bugs'
  371. self.patch_vendor_call()
  372. package, keyword = self.run_classify()
  373. self.assertEqual(package, 'headerpkg')
  374. self.assertEqual(keyword, 'bugs')
  375. class BounceMessagesTest(TestCase, DispatchTestHelperMixin):
  376. """
  377. Tests the proper handling of bounced emails.
  378. """
  379. def setUp(self):
  380. super(BounceMessagesTest, self).setUp()
  381. self.message = Message()
  382. self.message.add_header('Subject', 'bounce')
  383. PackageName.objects.create(name='dummy-package')
  384. self.subscribe_user_to_package('user@domain.com', 'dummy-package')
  385. self.user = UserEmailBounceStats.objects.get(email='user@domain.com')
  386. def create_bounce_address(self, to):
  387. """
  388. Helper method creating a bounce address for the given destination email
  389. """
  390. bounce_address = 'bounces+{date}@{distro_tracker_fqdn}'.format(
  391. date=timezone.now().date().strftime('%Y%m%d'),
  392. distro_tracker_fqdn=DISTRO_TRACKER_FQDN)
  393. return verp.encode(bounce_address, to)
  394. def add_sent(self, user, date):
  395. """
  396. Adds a sent mail record for the given user.
  397. """
  398. UserEmailBounceStats.objects.add_sent_for_user(email=user.email,
  399. date=date)
  400. def add_bounce(self, user, date):
  401. """
  402. Adds a bounced mail record for the given user.
  403. """
  404. UserEmailBounceStats.objects.add_bounce_for_user(email=user.email,
  405. date=date)
  406. def test_bounce_recorded(self):
  407. """
  408. Tests that a received bounce is recorded.
  409. """
  410. # Make sure the user has no prior bounce stats
  411. self.assertEqual(self.user.bouncestats_set.count(), 0)
  412. dispatch.handle_bounces(self.create_bounce_address(self.user.email))
  413. bounce_stats = self.user.bouncestats_set.all()
  414. self.assertEqual(bounce_stats.count(), 1)
  415. self.assertEqual(bounce_stats[0].date, timezone.now().date())
  416. self.assertEqual(bounce_stats[0].mails_bounced, 1)
  417. self.assertEqual(self.user.emailsettings.subscription_set.count(), 1)
  418. def test_bounce_over_limit(self):
  419. """
  420. Tests that all the user's subscriptions are dropped when too many
  421. bounces are received.
  422. """
  423. # Set up some prior bounces - one each day.
  424. date = timezone.now().date()
  425. for days in range(1, settings.DISTRO_TRACKER_MAX_DAYS_TOLERATE_BOUNCE):
  426. self.add_sent(self.user, date - timedelta(days=days))
  427. self.add_bounce(self.user, date - timedelta(days=days))
  428. # Set up a sent mail today.
  429. self.add_sent(self.user, date)
  430. # Make sure there were at least some subscriptions
  431. packages_subscribed_to = [
  432. subscription.package.name
  433. for subscription in self.user.emailsettings.subscription_set.all()
  434. ]
  435. self.assertTrue(len(packages_subscribed_to) > 0)
  436. # Receive a bounce message.
  437. dispatch.handle_bounces(self.create_bounce_address(self.user.email))
  438. # Assert that the user's subscriptions have been dropped.
  439. self.assertEqual(self.user.emailsettings.subscription_set.count(), 0)
  440. # A notification was sent to the user.
  441. self.assertEqual(len(mail.outbox), 1)
  442. self.assertIn(self.user.email, mail.outbox[0].to)
  443. # Check that the content of the email is correct.
  444. self.assertEqual(mail.outbox[0].body, distro_tracker_render_to_string(
  445. 'dispatch/unsubscribed-due-to-bounces-email.txt', {
  446. 'email': self.user.email,
  447. 'packages': packages_subscribed_to
  448. }
  449. ))
  450. def test_bounce_under_limit(self):
  451. """
  452. Tests that the user's subscriptions are not dropped when there are
  453. too many bounces for less days than tolerated.
  454. """
  455. # Set up some prior bounces - one each day.
  456. date = timezone.now().date()
  457. for days in range(1,
  458. settings.DISTRO_TRACKER_MAX_DAYS_TOLERATE_BOUNCE - 1):
  459. self.add_sent(self.user, date - timedelta(days=days))
  460. self.add_bounce(self.user, date - timedelta(days=days))
  461. # Set up a sent mail today.
  462. self.add_sent(self.user, date)
  463. # Make sure there were at least some subscriptions
  464. subscription_count = self.user.emailsettings.subscription_set.count()
  465. self.assertTrue(subscription_count > 0)
  466. # Receive a bounce message.
  467. dispatch.handle_bounces(self.create_bounce_address(self.user.email))
  468. # Assert that the user's subscriptions have not been dropped.
  469. self.assertEqual(self.user.emailsettings.subscription_set.count(),
  470. subscription_count)
  471. def test_bounces_not_every_day(self):
  472. """
  473. Tests that the user's subscriptions are not dropped when there is a day
  474. which had more sent messages.
  475. """
  476. date = timezone.now().date()
  477. for days in range(1, settings.DISTRO_TRACKER_MAX_DAYS_TOLERATE_BOUNCE):
  478. self.add_sent(self.user, date - timedelta(days=days))
  479. if days % 2 == 0:
  480. self.add_bounce(self.user, date - timedelta(days=days))
  481. # Set up a sent mail today.
  482. self.add_sent(self.user, date)
  483. # Make sure there were at least some subscriptions
  484. subscription_count = self.user.emailsettings.subscription_set.count()
  485. self.assertTrue(subscription_count > 0)
  486. # Receive a bounce message.
  487. dispatch.handle_bounces(self.create_bounce_address(self.user.email))
  488. # Assert that the user's subscriptions have not been dropped.
  489. self.assertEqual(self.user.emailsettings.subscription_set.count(),
  490. subscription_count)
  491. def test_bounce_recorded_with_differing_case(self):
  492. self.subscribe_user_to_package('SomeOne@domain.com', 'dummy-package')
  493. self.user = UserEmailBounceStats.objects.get(email='SomeOne@domain.com')
  494. self.assertEqual(self.user.bouncestats_set.count(), 0)
  495. dispatch.handle_bounces(
  496. self.create_bounce_address('someone@domain.com'))
  497. bounce_stats = self.user.bouncestats_set.all()
  498. self.assertEqual(bounce_stats.count(), 1)
  499. self.assertEqual(bounce_stats[0].date, timezone.now().date())
  500. self.assertEqual(bounce_stats[0].mails_bounced, 1)
  501. def test_bounce_handler_with_unknown_user_email(self):
  502. # This should just not generate any exception...
  503. dispatch.handle_bounces(
  504. self.create_bounce_address('unknown-user@domain.com'))
  505. class BounceStatsTest(TestCase):
  506. """
  507. Tests for the ``distro_tracker.mail.models`` handling users' bounce
  508. information.
  509. """
  510. def setUp(self):
  511. self.user = UserEmailBounceStats.objects.get(
  512. email=UserEmail.objects.create(email='user@domain.com'))
  513. self.package = PackageName.objects.create(name='dummy-package')
  514. def test_add_sent_message(self):
  515. """
  516. Tests that a new sent message record is correctly added.
  517. """
  518. date = timezone.now().date()
  519. UserEmailBounceStats.objects.add_sent_for_user(self.user.email, date)
  520. bounce_stats = self.user.bouncestats_set.all()
  521. self.assertEqual(bounce_stats.count(), 1)
  522. self.assertEqual(bounce_stats[0].date, timezone.now().date())
  523. self.assertEqual(bounce_stats[0].mails_sent, 1)
  524. def test_add_bounce_message(self):
  525. """
  526. Tests that a new bounced message record is correctly added.
  527. """
  528. date = timezone.now().date()
  529. UserEmailBounceStats.objects.add_bounce_for_user(self.user.email, date)
  530. bounce_stats = self.user.bouncestats_set.all()
  531. self.assertEqual(bounce_stats.count(), 1)
  532. self.assertEqual(bounce_stats[0].date, timezone.now().date())
  533. self.assertEqual(bounce_stats[0].mails_bounced, 1)
  534. def test_number_of_records_limited(self):
  535. """
  536. Tests that only as many records as the number of tolerated bounce days
  537. are kept.
  538. """
  539. days = settings.DISTRO_TRACKER_MAX_DAYS_TOLERATE_BOUNCE
  540. current_date = timezone.now().date()
  541. dates = [
  542. current_date + timedelta(days=delta)
  543. for delta in range(1, days + 5)
  544. ]
  545. for date in dates:
  546. UserEmailBounceStats.objects.add_bounce_for_user(
  547. self.user.email, date)
  548. bounce_stats = self.user.bouncestats_set.all()
  549. # Limited number
  550. self.assertEqual(bounce_stats.count(),
  551. settings.DISTRO_TRACKER_MAX_DAYS_TOLERATE_BOUNCE)
  552. # Only the most recent dates are kept.
  553. bounce_stats_dates = [info.date for info in bounce_stats]
  554. for date in dates[-days:]:
  555. self.assertIn(date, bounce_stats_dates)
  556. class DispatchToTeamsTests(DispatchTestHelperMixin, TestCase):
  557. def setUp(self):
  558. super(DispatchToTeamsTests, self).setUp()
  559. self.password = 'asdf'
  560. self.user = User.objects.create_user(
  561. main_email='user@domain.com', password=self.password,
  562. first_name='', last_name='')
  563. self.team = Team.objects.create_with_slug(
  564. owner=self.user, name="Team name")
  565. self.team.add_members([self.user.emails.all()[0]])
  566. self.package = PackageName.objects.create(name='dummy-package')
  567. self.team.packages.add(self.package)
  568. self.user_email = UserEmail.objects.create(email='other@domain.com')
  569. # Setup a message which will be sent to the package
  570. self.clear_message()
  571. self.from_email = 'dummy-email@domain.com'
  572. self.set_package_name('dummy-package')
  573. self.add_header('From', 'Real Name <{from_email}>'.format(
  574. from_email=self.from_email))
  575. self.add_header('Subject', 'Some subject')
  576. self.add_header('X-Loop', 'owner@bugs.debian.org')
  577. self.add_header('X-Distro-Tracker-Approved', '1')
  578. self.set_message_content('message content')
  579. def test_team_muted(self):
  580. """
  581. Tests that a message is not forwarded to the user when he has muted
  582. the team.
  583. """
  584. email = self.user.main_email
  585. membership = self.team.team_membership_set.get(user_email__email=email)
  586. membership.set_keywords(self.package, ['default'])
  587. membership.muted = True
  588. membership.save()
  589. self.run_forward()
  590. self.assertEqual(0, len(mail.outbox))
  591. def test_message_forwarded(self):
  592. """
  593. Tests that the message is forwarded to a team member when he has the
  594. correct keyword.
  595. """
  596. email = self.user.main_email
  597. membership = self.team.team_membership_set.get(user_email__email=email)
  598. membership.set_keywords(self.package, ['default'])
  599. self.run_forward()
  600. self.assert_message_forwarded_to(email)
  601. def test_message_not_forwarded_no_keyword(self):
  602. """
  603. Tests that a message is not forwarded to a team member that does not
  604. have the messages keyword set.
  605. """
  606. email = self.user.main_email
  607. membership = self.team.team_membership_set.get(user_email__email=email)
  608. membership.set_keywords(
  609. self.package,
  610. [k.name for k in Keyword.objects.exclude(name='default')])
  611. self.run_forward()
  612. self.assertEqual(0, len(mail.outbox))
  613. def test_forwarded_message_correct_headers(self):
  614. """
  615. Tests that the headers of the forwarded message are correctly set.
  616. """
  617. email = self.user.main_email
  618. membership = self.team.team_membership_set.get(user_email__email=email)
  619. membership.set_keywords(self.package, ['default'])
  620. self.run_forward()
  621. self.assert_header_equal('X-Distro-Tracker-Keyword', 'default')
  622. self.assert_header_equal('X-Distro-Tracker-Team', self.team.slug)
  623. self.assert_header_equal('X-Distro-Tracker-Package', self.package.name)
  624. def test_forward_multiple_teams(self):
  625. """
  626. Tests that a user gets the same message multiple times when he is a
  627. member of two teams that both have the same package.
  628. """
  629. new_team = Team.objects.create_with_slug(
  630. owner=self.user, name="Other team")
  631. new_team.packages.add(self.package)
  632. new_team.add_members([self.user.emails.all()[0]])
  633. self.run_forward()
  634. self.assertEqual(2, len(mail.outbox))
  635. for message, team in zip(mail.outbox, Team.objects.all()):
  636. message = message.message()
  637. self.assertEqual(message['X-Distro-Tracker-Team'], team.slug)
  638. def test_package_muted(self):
  639. """
  640. Tests that when the team membership is not muted, but the package
  641. which is a part of the membership is, no message is forwarded.
  642. """
  643. email = self.user.main_email
  644. membership = self.team.team_membership_set.get(user_email__email=email)
  645. membership.set_keywords(self.package, ['default'])
  646. membership.mute_package(self.package)
  647. self.run_forward()
  648. self.assertEqual(0, len(mail.outbox))