tests.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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. class TestManageView(APITestCase):
  132. def setUp(self):
  133. self.contrib = Contrib.objects.create(
  134. name='John',
  135. phone='010101010101',
  136. contrib_type=Contrib.CONTRIB_CONNECT,
  137. privacy_coordinates=True,
  138. latitude=0.5,
  139. longitude=0.5,
  140. )
  141. self.token = ContribTokenManager().mk_token(self.contrib)
  142. def test_manage_with_token(self):
  143. # No token
  144. response = self.client.get('/map/manage/{}'.format(self.contrib.pk))
  145. self.assertEqual(response.status_code, 403)
  146. # Garbage token
  147. response = self.client.get(
  148. '/map/manage/{}?token=burp'.format(self.contrib.pk))
  149. self.assertEqual(response.status_code, 403)
  150. # Valid token, but for another contrib
  151. contrib2 = Contrib.objects.create(
  152. name='John2',
  153. phone='010101010101',
  154. contrib_type=Contrib.CONTRIB_CONNECT,
  155. privacy_coordinates=True,
  156. latitude=0.5,
  157. longitude=0.5,
  158. )
  159. token2 = ContribTokenManager().mk_token(contrib2)
  160. response = self.client.get('/map/manage/{}?token={}'.format(
  161. self.contrib.pk, token2))
  162. self.assertEqual(response.status_code, 403)
  163. # Normal legitimate access case
  164. response = self.client.get(
  165. '/map/manage/{}?token={}'.format(self.contrib.pk, self.token))
  166. self.assertEqual(response.status_code, 200)
  167. # Deleted contrib
  168. Contrib.objects.all().delete()
  169. response = self.client.get(
  170. '/map/manage/{}?token={}'.format(self.contrib.pk, self.token))
  171. self.assertEqual(response.status_code, 404)
  172. def test_delete(self):
  173. response = self.client.post(
  174. '/map/manage/{}?token={}'.format(self.contrib.pk, self.token),
  175. {'action': 'delete'})
  176. self.assertEqual(response.status_code, 302)
  177. self.assertFalse(Contrib.objects.filter(pk=self.contrib.pk).exists())
  178. def test_renew(self):
  179. self.contrib.date = datetime.datetime(2009, 10, 10, tzinfo=pytz.utc)
  180. self.contrib.expiration_date = datetime.datetime(2010, 10, 10, tzinfo=pytz.utc)
  181. self.contrib.save()
  182. with freeze_time('12-12-2100', tz_offset=0):
  183. response = self.client.post(
  184. '/map/manage/{}?token={}'.format(self.contrib.pk, self.token),
  185. {'action': 'renew'})
  186. self.assertEqual(response.status_code, 200)
  187. self.contrib = Contrib.objects.get(pk=self.contrib.pk) # refresh
  188. self.assertEqual(
  189. self.contrib.expiration_date.date(),
  190. datetime.date(2101, 12, 12))
  191. class TestForms(TestCase):
  192. valid_data = {
  193. 'roof': True,
  194. 'privacy_place_details': True,
  195. 'privacy_coordinates': True,
  196. 'orientations': ['N'],
  197. 'orientation': 'all',
  198. 'name': 'JohnCleese',
  199. 'longitude': -1.553621,
  200. 'email': 'foo@example.com',
  201. 'phone': '0202020202',
  202. 'latitude': 47.218371,
  203. 'floor_total': '2',
  204. 'floor': 1,
  205. 'contrib_type': 'connect',
  206. 'connect_local': 'on',
  207. }
  208. def test_contact_validation(self):
  209. no_contact, phone_contact, email_contact, both_contact = [
  210. self.valid_data.copy() for i in range(4)]
  211. del phone_contact['email']
  212. del email_contact['phone']
  213. del no_contact['phone']
  214. del no_contact['email']
  215. both_contact.update(phone_contact)
  216. both_contact.update(email_contact)
  217. self.assertFalse(PublicContribForm(no_contact).is_valid())
  218. self.assertTrue(PublicContribForm(phone_contact).is_valid())
  219. self.assertTrue(PublicContribForm(email_contact).is_valid())
  220. self.assertTrue(PublicContribForm(both_contact).is_valid())
  221. def test_floors_validation(self):
  222. invalid_floors = self.valid_data.copy()
  223. invalid_floors['floor'] = 2
  224. invalid_floors['floor_total'] = 1
  225. self.assertFalse(PublicContribForm(invalid_floors).is_valid())
  226. self.assertTrue(PublicContribForm(self.valid_data).is_valid())
  227. invalid_floors['floor'] = None
  228. invalid_floors['floor_total'] = None
  229. self.assertTrue(PublicContribForm(invalid_floors).is_valid())
  230. def test_share_fields_validation(self):
  231. data = self.valid_data.copy()
  232. data['contrib_type'] = 'share'
  233. self.assertFalse(PublicContribForm(data).is_valid())
  234. data['access_type'] = 'cable'
  235. self.assertTrue(PublicContribForm(data).is_valid())
  236. @override_settings(NOTIFICATION_EMAILS=['foo@example.com'])
  237. def test_add_contrib_like_a_robot(self):
  238. robot_data = self.valid_data.copy()
  239. robot_data['human_field'] = 'should contain no value'
  240. response = self.client.post('/map/contribute', robot_data)
  241. self.assertEqual(response.status_code, 403)
  242. self.assertEqual(len(mail.outbox), 0)
  243. class TestDataImport(TestCase):
  244. fixtures = ['bottle_data.yaml']
  245. @classmethod
  246. def setUpClass(cls, *args, **kwargs):
  247. # Silence the warnings about naive datetimes contained in the yaml.
  248. with warnings.catch_warnings(): # Scope warn catch to this block
  249. warnings.simplefilter('ignore', RuntimeWarning)
  250. return super().setUpClass(*args, **kwargs)
  251. def test_re_save(self):
  252. for contrib in Contrib.objects.all():
  253. contrib.full_clean()
  254. contrib.save()
  255. class URLTokenManagerTests(TestCase):
  256. def test_sign_unsign_ok(self):
  257. input_data = {'foo': 12}
  258. at = URLTokenManager().sign(input_data)
  259. output_data = URLTokenManager().unsign(at)
  260. self.assertEqual(input_data, output_data)
  261. def test_sign_unsign_wrong_sig(self):
  262. with self.assertRaises(BadSignature):
  263. URLTokenManager().unsign(
  264. b'eyJmb28iOiAxfTpvUFZ1Q3FsSldtQ2htMXJBMmx5VFV0ZWxDLWM')
  265. class ContribTokenManagerTests(TestCase):
  266. def test_sign_unsign_ok(self):
  267. Contrib.objects.create(
  268. name='John2',
  269. phone='010101020101',
  270. contrib_type=Contrib.CONTRIB_CONNECT,
  271. privacy_coordinates=True,
  272. latitude=0.1,
  273. longitude=0.12,
  274. )
  275. contrib = Contrib.objects.all().first()
  276. manager = ContribTokenManager()
  277. token = manager.mk_token(contrib)
  278. self.assertEqual(
  279. manager.get_instance_if_allowed(token, contrib.pk),
  280. contrib)