tests.py 12 KB

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