tests.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. import datetime
  2. import json
  3. import warnings
  4. from django.core import mail
  5. from django.core.management import call_command
  6. from django.core.signing import BadSignature
  7. from django.contrib.auth.models import User
  8. from django.test import TestCase, Client, override_settings
  9. from freezegun import freeze_time
  10. import pytz
  11. from contribmap.models import Contrib
  12. from contribmap.forms import PublicContribForm
  13. from contribmap.tokens import ContribTokenManager, URLTokenManager
  14. class APITestClient(Client):
  15. def json_get(self, *args, **kwargs):
  16. """ Annotate the response with a .data containing parsed JSON
  17. """
  18. response = super().get(*args, **kwargs)
  19. response.data = json.loads(response.content.decode('utf-8'))
  20. return response
  21. class APITestCase(TestCase):
  22. def setUp(self):
  23. super().setUp()
  24. self.client = APITestClient()
  25. class TestContrib(TestCase):
  26. def test_comma_separatedcharfield(self):
  27. co = Contrib(name='foo', orientations=['SO', 'NE'],
  28. contrib_type=Contrib.CONTRIB_CONNECT,
  29. latitude=0.5, longitude=0.5,
  30. )
  31. co.save()
  32. self.assertEqual(
  33. Contrib.objects.get(name='foo').orientations,
  34. ['SO', 'NE'])
  35. co.orientations = ['S']
  36. co.save()
  37. class TestContribPrivacy(TestCase):
  38. def test_always_private_field(self):
  39. c = Contrib.objects.create(
  40. name='John',
  41. phone='010101010101',
  42. contrib_type=Contrib.CONTRIB_CONNECT,
  43. latitude=0.5,
  44. longitude=0.5,
  45. )
  46. self.assertEqual(c.get_public_field('phone'), None)
  47. def test_public_field(self):
  48. c = Contrib.objects.create(
  49. name='John',
  50. phone='010101010101',
  51. contrib_type=Contrib.CONTRIB_CONNECT,
  52. privacy_name=True,
  53. latitude=0.5,
  54. longitude=0.5,
  55. )
  56. self.assertEqual(c.get_public_field('name'), 'John')
  57. def test_public_callable_field(self):
  58. c = Contrib.objects.create(
  59. name='John',
  60. phone='010101010101',
  61. orientations=['N'],
  62. contrib_type=Contrib.CONTRIB_CONNECT,
  63. privacy_name=True,
  64. latitude=0.5,
  65. longitude=0.5,
  66. )
  67. self.assertEqual(c.get_public_field('angles'), [[-23, 22]])
  68. def test_private_field(self):
  69. c = Contrib.objects.create(
  70. name='John',
  71. phone='010101010101',
  72. contrib_type=Contrib.CONTRIB_CONNECT,
  73. latitude=0.5,
  74. longitude=0.5,
  75. )
  76. self.assertEqual(c.privacy_name, False)
  77. self.assertEqual(c.get_public_field('name'), None)
  78. class TestContribQuerySet(TestCase):
  79. def test_expired(self):
  80. with freeze_time('12-11-2100', tz_offset=0):
  81. Contrib.objects.create(
  82. name='foo', orientations=['S'],
  83. contrib_type=Contrib.CONTRIB_CONNECT,
  84. latitude=0.5, longitude=0.5)
  85. # one year and one month later
  86. with freeze_time('12-12-2101', tz_offset=0):
  87. Contrib.objects.create(
  88. name='bar', orientations=['S'],
  89. contrib_type=Contrib.CONTRIB_CONNECT,
  90. latitude=0.5, longitude=0.5)
  91. expired = Contrib.objects.expired()
  92. self.assertEqual(expired.count(), 1)
  93. self.assertEqual(expired.first().name, 'foo')
  94. def test_expired_in_days(self):
  95. Contrib.objects.create(
  96. name='foo', orientations=['S'],
  97. contrib_type=Contrib.CONTRIB_CONNECT,
  98. latitude=0.5, longitude=0.5)
  99. self.assertEqual(Contrib.objects.expired_in_days(0).count(), 0)
  100. self.assertEqual(Contrib.objects.expired_in_days(366).count(), 1)
  101. def test_expires_in_days(self):
  102. with freeze_time('12-11-2101 12:00', tz_offset=0):
  103. Contrib.objects.create(
  104. name='foo', orientations=['S'],
  105. contrib_type=Contrib.CONTRIB_CONNECT,
  106. latitude=0.5, longitude=0.5)
  107. self.assertEqual(Contrib.objects.expires_in_days(364).count(), 0)
  108. self.assertEqual(Contrib.objects.expires_in_days(365).count(), 1)
  109. self.assertEqual(Contrib.objects.expires_in_days(366).count(), 0)
  110. # One year, one hour and two minutes later
  111. # (check that minutes/hours are ignored)
  112. with freeze_time('12-11-2101 13:02', tz_offset=0):
  113. self.assertEqual(Contrib.objects.expires_in_days(365).count(), 1)
  114. class TestViews(APITestCase):
  115. def mk_contrib_post_data(self, *args, **kwargs):
  116. post_data = {
  117. 'roof': True,
  118. 'privacy_place_details': True,
  119. 'privacy_coordinates': True,
  120. 'phone': '0202020202',
  121. 'orientations': ('N', 'NO', 'O', 'SO', 'S', 'SE', 'E', 'NE'),
  122. 'orientation': 'all',
  123. 'name': 'JohnCleese',
  124. 'longitude': -1.553621,
  125. 'latitude': 47.218371,
  126. 'floor_total': '2',
  127. 'floor': 1,
  128. 'email': 'coucou@example.com',
  129. 'contrib_type': 'connect',
  130. 'connect_local': 'on',
  131. }
  132. post_data.update(kwargs)
  133. return post_data
  134. def test_public_json(self):
  135. response = self.client.json_get('/map/public.json')
  136. self.assertEqual(response.status_code, 200)
  137. self.assertEqual(len(response.data['features']), 0)
  138. Contrib.objects.create(
  139. name='John',
  140. phone='010101010101',
  141. contrib_type=Contrib.CONTRIB_CONNECT,
  142. privacy_coordinates=True,
  143. latitude=0.5,
  144. longitude=0.5,
  145. )
  146. response = self.client.json_get('/map/public.json')
  147. self.assertEqual(response.status_code, 200)
  148. self.assertEqual(len(response.data['features']), 1)
  149. def test_private_json(self):
  150. self.client.force_login(
  151. User.objects.create(username='foo', is_staff=False))
  152. response = self.client.get('/map/private.json')
  153. self.assertEqual(response.status_code, 403)
  154. def test_private_json_staff(self):
  155. self.client.force_login(
  156. User.objects.create(username='foo', is_staff=True))
  157. response = self.client.get('/map/private.json')
  158. self.assertEqual(response.status_code, 200)
  159. @override_settings(NOTIFICATION_EMAILS=['foo@example.com'])
  160. def test_add_contrib_sends_moderator_email(self):
  161. post_data = self.mk_contrib_post_data({'name': 'JohnCleese'})
  162. del post_data['email']
  163. response = self.client.post('/map/contribute', post_data)
  164. self.assertEqual(response.status_code, 302)
  165. self.assertEqual(len(mail.outbox), 1)
  166. self.assertIn('JohnCleese', mail.outbox[0].subject)
  167. self.assertIn('JohnCleese', mail.outbox[0].body)
  168. self.assertEqual(mail.outbox[0].recipients(), ['foo@example.com'])
  169. def test_add_contrib_sends_no_author_email(self):
  170. # Send no email if author did not mentioned an email
  171. post_data = self.mk_contrib_post_data()
  172. del post_data['email']
  173. response = self.client.post('/map/contribute', post_data)
  174. self.assertEqual(response.status_code, 302)
  175. self.assertEqual(len(mail.outbox), 0)
  176. def test_add_contrib_sends_author_email(self):
  177. # Send no email if author did not mentioned an email
  178. response = self.client.post(
  179. '/map/contribute',
  180. self.mk_contrib_post_data(email='author@example.com'))
  181. self.assertEqual(response.status_code, 302)
  182. self.assertEqual(len(mail.outbox), 1)
  183. class TestManageView(APITestCase):
  184. def setUp(self):
  185. self.contrib = Contrib.objects.create(
  186. name='John',
  187. phone='010101010101',
  188. contrib_type=Contrib.CONTRIB_CONNECT,
  189. privacy_coordinates=True,
  190. latitude=0.5,
  191. longitude=0.5,
  192. )
  193. self.token = ContribTokenManager().mk_token(self.contrib)
  194. def test_manage_with_token(self):
  195. # No token
  196. response = self.client.get('/map/manage/{}'.format(self.contrib.pk))
  197. self.assertEqual(response.status_code, 403)
  198. # Garbage token
  199. response = self.client.get(
  200. '/map/manage/{}?token=burp'.format(self.contrib.pk))
  201. self.assertEqual(response.status_code, 403)
  202. # Valid token, but for another contrib
  203. contrib2 = Contrib.objects.create(
  204. name='John2',
  205. phone='010101010101',
  206. contrib_type=Contrib.CONTRIB_CONNECT,
  207. privacy_coordinates=True,
  208. latitude=0.5,
  209. longitude=0.5,
  210. )
  211. token2 = ContribTokenManager().mk_token(contrib2)
  212. response = self.client.get('/map/manage/{}?token={}'.format(
  213. self.contrib.pk, token2))
  214. self.assertEqual(response.status_code, 403)
  215. # Normal legitimate access case
  216. response = self.client.get(
  217. '/map/manage/{}?token={}'.format(self.contrib.pk, self.token))
  218. self.assertEqual(response.status_code, 200)
  219. # Deleted contrib
  220. Contrib.objects.all().delete()
  221. response = self.client.get(
  222. '/map/manage/{}?token={}'.format(self.contrib.pk, self.token))
  223. self.assertEqual(response.status_code, 404)
  224. def test_delete(self):
  225. response = self.client.post(
  226. '/map/manage/{}?token={}'.format(self.contrib.pk, self.token),
  227. {'action': 'delete'})
  228. self.assertEqual(response.status_code, 302)
  229. self.assertFalse(Contrib.objects.filter(pk=self.contrib.pk).exists())
  230. def test_renew(self):
  231. self.contrib.date = datetime.datetime(2009, 10, 10, tzinfo=pytz.utc)
  232. self.contrib.expiration_date = datetime.datetime(2010, 10, 10, tzinfo=pytz.utc)
  233. self.contrib.save()
  234. with freeze_time('12-12-2100', tz_offset=0):
  235. response = self.client.post(
  236. '/map/manage/{}?token={}'.format(self.contrib.pk, self.token),
  237. {'action': 'renew'})
  238. self.assertEqual(response.status_code, 200)
  239. self.contrib = Contrib.objects.get(pk=self.contrib.pk) # refresh
  240. self.assertEqual(
  241. self.contrib.expiration_date.date(),
  242. datetime.date(2101, 12, 12))
  243. class TestForms(TestCase):
  244. valid_data = {
  245. 'roof': True,
  246. 'privacy_place_details': True,
  247. 'privacy_coordinates': True,
  248. 'orientations': ['N'],
  249. 'orientation': 'all',
  250. 'name': 'JohnCleese',
  251. 'longitude': -1.553621,
  252. 'email': 'foo@example.com',
  253. 'phone': '0202020202',
  254. 'latitude': 47.218371,
  255. 'floor_total': '2',
  256. 'floor': 1,
  257. 'contrib_type': 'connect',
  258. 'connect_local': 'on',
  259. }
  260. def test_contact_validation(self):
  261. no_contact, phone_contact, email_contact, both_contact = [
  262. self.valid_data.copy() for i in range(4)]
  263. del phone_contact['email']
  264. del email_contact['phone']
  265. del no_contact['phone']
  266. del no_contact['email']
  267. both_contact.update(phone_contact)
  268. both_contact.update(email_contact)
  269. self.assertFalse(PublicContribForm(no_contact).is_valid())
  270. self.assertTrue(PublicContribForm(phone_contact).is_valid())
  271. self.assertTrue(PublicContribForm(email_contact).is_valid())
  272. self.assertTrue(PublicContribForm(both_contact).is_valid())
  273. def test_floors_validation(self):
  274. invalid_floors = self.valid_data.copy()
  275. invalid_floors['floor'] = 2
  276. invalid_floors['floor_total'] = 1
  277. self.assertFalse(PublicContribForm(invalid_floors).is_valid())
  278. self.assertTrue(PublicContribForm(self.valid_data).is_valid())
  279. invalid_floors['floor'] = None
  280. invalid_floors['floor_total'] = None
  281. self.assertTrue(PublicContribForm(invalid_floors).is_valid())
  282. def test_share_fields_validation(self):
  283. data = self.valid_data.copy()
  284. data['contrib_type'] = 'share'
  285. self.assertFalse(PublicContribForm(data).is_valid())
  286. data['access_type'] = 'cable'
  287. self.assertTrue(PublicContribForm(data).is_valid())
  288. @override_settings(NOTIFICATION_EMAILS=['foo@example.com'])
  289. def test_add_contrib_like_a_robot(self):
  290. robot_data = self.valid_data.copy()
  291. robot_data['human_field'] = 'should contain no value'
  292. response = self.client.post('/map/contribute', robot_data)
  293. self.assertEqual(response.status_code, 403)
  294. self.assertEqual(len(mail.outbox), 0)
  295. class TestDataImport(TestCase):
  296. fixtures = ['bottle_data.yaml']
  297. @classmethod
  298. def setUpClass(cls, *args, **kwargs):
  299. # Silence the warnings about naive datetimes contained in the yaml.
  300. with warnings.catch_warnings(): # Scope warn catch to this block
  301. warnings.simplefilter('ignore', RuntimeWarning)
  302. return super().setUpClass(*args, **kwargs)
  303. def test_re_save(self):
  304. for contrib in Contrib.objects.all():
  305. contrib.full_clean()
  306. contrib.save()
  307. class URLTokenManagerTests(TestCase):
  308. def test_sign_unsign_ok(self):
  309. input_data = {'foo': 12}
  310. at = URLTokenManager().sign(input_data)
  311. output_data = URLTokenManager().unsign(at)
  312. self.assertEqual(input_data, output_data)
  313. def test_sign_unsign_wrong_sig(self):
  314. with self.assertRaises(BadSignature):
  315. URLTokenManager().unsign(
  316. b'eyJmb28iOiAxfTpvUFZ1Q3FsSldtQ2htMXJBMmx5VFV0ZWxDLWM')
  317. class ContribTokenManagerTests(TestCase):
  318. def test_sign_unsign_ok(self):
  319. Contrib.objects.create(
  320. name='John2',
  321. phone='010101020101',
  322. contrib_type=Contrib.CONTRIB_CONNECT,
  323. privacy_coordinates=True,
  324. latitude=0.1,
  325. longitude=0.12,
  326. )
  327. contrib = Contrib.objects.all().first()
  328. manager = ContribTokenManager()
  329. token = manager.mk_token(contrib)
  330. self.assertEqual(
  331. manager.get_instance_if_allowed(token, contrib.pk),
  332. contrib)
  333. class TestManagementCommands(TestCase):
  334. def setUp(self):
  335. contrib = Contrib.objects.create(
  336. name='John',
  337. email='foo@example.com',
  338. contrib_type=Contrib.CONTRIB_CONNECT,
  339. latitude=0.5,
  340. longitude=0.5,
  341. )
  342. contrib.expiration_date = datetime.datetime(
  343. 2010, 10, 10, 1, 0, tzinfo=pytz.utc)
  344. contrib.save()
  345. @override_settings(DATA_EXPIRATION_REMINDERS=[10])
  346. def test_send_expiration_reminders(self):
  347. # 11 days before (should not send)
  348. with freeze_time('29-09-2010', tz_offset=0):
  349. call_command('send_expiration_reminders')
  350. self.assertEqual(Contrib.objects.count(), 1)
  351. # 9 days before (should not send)
  352. with freeze_time('01-10-2010', tz_offset=0):
  353. call_command('send_expiration_reminders')
  354. self.assertEqual(Contrib.objects.count(), 1)
  355. # 10 days before (should send)
  356. with freeze_time('30-09-2010', tz_offset=0):
  357. call_command('send_expiration_reminders', '--dry-run')
  358. self.assertEqual(Contrib.objects.count(), 1)
  359. call_command('send_expiration_reminders')
  360. self.assertEqual(len(mail.outbox), 1)
  361. def test_delete_expired_contribs(self):
  362. # 1 days before expiration
  363. with freeze_time('09-09-2010', tz_offset=0):
  364. call_command('delete_expired_contribs')
  365. self.assertEqual(Contrib.objects.count(), 1)
  366. # expiration day
  367. with freeze_time('10-10-2010 23:59', tz_offset=0):
  368. call_command('delete_expired_contribs', '--dry-run')
  369. self.assertEqual(Contrib.objects.count(), 1)
  370. call_command('delete_expired_contribs')
  371. self.assertEqual(Contrib.objects.count(), 0)
  372. self.setUp()
  373. # 1 day after expiration
  374. with freeze_time('11-10-2010', tz_offset=0):
  375. call_command('delete_expired_contribs')
  376. self.assertEqual(Contrib.objects.count(), 0)