import datetime import json import warnings from django.core import mail from django.core.management import call_command from django.core.signing import BadSignature from django.contrib.auth.models import User from django.test import TestCase, Client, override_settings from freezegun import freeze_time import pytz from contribmap.models import Contrib from contribmap.forms import PublicContribForm from contribmap.tokens import ContribTokenManager, URLTokenManager class APITestClient(Client): def json_get(self, *args, **kwargs): """ Annotate the response with a .data containing parsed JSON """ response = super().get(*args, **kwargs) response.data = json.loads(response.content.decode('utf-8')) return response class APITestCase(TestCase): def setUp(self): super().setUp() self.client = APITestClient() class TestContrib(TestCase): def test_comma_separatedcharfield(self): co = Contrib(name='foo', orientations=['SO', 'NE'], contrib_type=Contrib.CONTRIB_CONNECT, latitude=0.5, longitude=0.5, ) co.save() self.assertEqual( Contrib.objects.get(name='foo').orientations, ['SO', 'NE']) co.orientations = ['S'] co.save() class TestContribPrivacy(TestCase): def test_always_private_field(self): c = Contrib.objects.create( name='John', phone='010101010101', contrib_type=Contrib.CONTRIB_CONNECT, latitude=0.5, longitude=0.5, ) self.assertEqual(c.get_public_field('phone'), None) def test_public_field(self): c = Contrib.objects.create( name='John', phone='010101010101', contrib_type=Contrib.CONTRIB_CONNECT, privacy_name=True, latitude=0.5, longitude=0.5, ) self.assertEqual(c.get_public_field('name'), 'John') def test_public_callable_field(self): c = Contrib.objects.create( name='John', phone='010101010101', orientations=['N'], contrib_type=Contrib.CONTRIB_CONNECT, privacy_name=True, latitude=0.5, longitude=0.5, ) self.assertEqual(c.get_public_field('angles'), [[-23, 22]]) def test_private_field(self): c = Contrib.objects.create( name='John', phone='010101010101', contrib_type=Contrib.CONTRIB_CONNECT, latitude=0.5, longitude=0.5, ) self.assertEqual(c.privacy_name, False) self.assertEqual(c.get_public_field('name'), None) class TestContribQuerySet(TestCase): def test_expired(self): with freeze_time('12-11-2100', tz_offset=0): Contrib.objects.create( name='foo', orientations=['S'], contrib_type=Contrib.CONTRIB_CONNECT, latitude=0.5, longitude=0.5) # one year and one month later with freeze_time('12-12-2101', tz_offset=0): Contrib.objects.create( name='bar', orientations=['S'], contrib_type=Contrib.CONTRIB_CONNECT, latitude=0.5, longitude=0.5) expired = Contrib.objects.expired() self.assertEqual(expired.count(), 1) self.assertEqual(expired.first().name, 'foo') def test_expired_in_days(self): Contrib.objects.create( name='foo', orientations=['S'], contrib_type=Contrib.CONTRIB_CONNECT, latitude=0.5, longitude=0.5) self.assertEqual(Contrib.objects.expired_in_days(0).count(), 0) self.assertEqual(Contrib.objects.expired_in_days(366).count(), 1) def test_expires_in_days(self): with freeze_time('12-11-2101 12:00', tz_offset=0): Contrib.objects.create( name='foo', orientations=['S'], contrib_type=Contrib.CONTRIB_CONNECT, latitude=0.5, longitude=0.5) self.assertEqual(Contrib.objects.expires_in_days(364).count(), 0) self.assertEqual(Contrib.objects.expires_in_days(365).count(), 1) self.assertEqual(Contrib.objects.expires_in_days(366).count(), 0) # One year, one hour and two minutes later # (check that minutes/hours are ignored) with freeze_time('12-11-2101 13:02', tz_offset=0): self.assertEqual(Contrib.objects.expires_in_days(365).count(), 1) class TestViews(APITestCase): def mk_contrib_post_data(self, *args, **kwargs): post_data = { 'roof': True, 'privacy_place_details': True, 'privacy_coordinates': True, 'phone': '0202020202', 'orientations': ('N', 'NO', 'O', 'SO', 'S', 'SE', 'E', 'NE'), 'orientation': 'all', 'name': 'JohnCleese', 'longitude': -1.553621, 'latitude': 47.218371, 'floor_total': '2', 'floor': 1, 'email': 'coucou@example.com', 'contrib_type': 'connect', 'connect_local': 'on', } post_data.update(kwargs) return post_data def test_public_json(self): response = self.client.json_get('/map/public.json') self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data['features']), 0) Contrib.objects.create( name='John', phone='010101010101', contrib_type=Contrib.CONTRIB_CONNECT, privacy_coordinates=True, latitude=0.5, longitude=0.5, ) response = self.client.json_get('/map/public.json') self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data['features']), 1) def test_private_json(self): self.client.force_login( User.objects.create(username='foo', is_staff=False)) response = self.client.get('/map/private.json') self.assertEqual(response.status_code, 403) def test_private_json_staff(self): self.client.force_login( User.objects.create(username='foo', is_staff=True)) response = self.client.get('/map/private.json') self.assertEqual(response.status_code, 200) @override_settings(NOTIFICATION_EMAILS=['foo@example.com']) def test_add_contrib_sends_moderator_email(self): post_data = self.mk_contrib_post_data({'name': 'JohnCleese'}) del post_data['email'] response = self.client.post('/map/contribute', post_data) self.assertEqual(response.status_code, 302) self.assertEqual(len(mail.outbox), 1) self.assertIn('JohnCleese', mail.outbox[0].subject) self.assertIn('JohnCleese', mail.outbox[0].body) self.assertEqual(mail.outbox[0].recipients(), ['foo@example.com']) def test_add_contrib_sends_no_author_email(self): # Send no email if author did not mentioned an email post_data = self.mk_contrib_post_data() del post_data['email'] response = self.client.post('/map/contribute', post_data) self.assertEqual(response.status_code, 302) self.assertEqual(len(mail.outbox), 0) def test_add_contrib_sends_author_email(self): # Send no email if author did not mentioned an email response = self.client.post( '/map/contribute', self.mk_contrib_post_data(email='author@example.com')) self.assertEqual(response.status_code, 302) self.assertEqual(len(mail.outbox), 1) class TestManageView(APITestCase): def setUp(self): self.contrib = Contrib.objects.create( name='John', phone='010101010101', contrib_type=Contrib.CONTRIB_CONNECT, privacy_coordinates=True, latitude=0.5, longitude=0.5, ) self.token = ContribTokenManager().mk_token(self.contrib) def test_manage_with_token(self): # No token response = self.client.get('/map/manage/{}'.format(self.contrib.pk)) self.assertEqual(response.status_code, 403) # Garbage token response = self.client.get( '/map/manage/{}?token=burp'.format(self.contrib.pk)) self.assertEqual(response.status_code, 403) # Valid token, but for another contrib contrib2 = Contrib.objects.create( name='John2', phone='010101010101', contrib_type=Contrib.CONTRIB_CONNECT, privacy_coordinates=True, latitude=0.5, longitude=0.5, ) token2 = ContribTokenManager().mk_token(contrib2) response = self.client.get('/map/manage/{}?token={}'.format( self.contrib.pk, token2)) self.assertEqual(response.status_code, 403) # Normal legitimate access case response = self.client.get( '/map/manage/{}?token={}'.format(self.contrib.pk, self.token)) self.assertEqual(response.status_code, 200) # Deleted contrib Contrib.objects.all().delete() response = self.client.get( '/map/manage/{}?token={}'.format(self.contrib.pk, self.token)) self.assertEqual(response.status_code, 404) def test_delete(self): response = self.client.post( '/map/manage/{}?token={}'.format(self.contrib.pk, self.token), {'action': 'delete'}) self.assertEqual(response.status_code, 302) self.assertFalse(Contrib.objects.filter(pk=self.contrib.pk).exists()) def test_renew(self): self.contrib.date = datetime.datetime(2009, 10, 10, tzinfo=pytz.utc) self.contrib.expiration_date = datetime.datetime(2010, 10, 10, tzinfo=pytz.utc) self.contrib.save() with freeze_time('12-12-2100', tz_offset=0): response = self.client.post( '/map/manage/{}?token={}'.format(self.contrib.pk, self.token), {'action': 'renew'}) self.assertEqual(response.status_code, 200) self.contrib = Contrib.objects.get(pk=self.contrib.pk) # refresh self.assertEqual( self.contrib.expiration_date.date(), datetime.date(2101, 12, 12)) class TestForms(TestCase): valid_data = { 'roof': True, 'privacy_place_details': True, 'privacy_coordinates': True, 'orientations': ['N'], 'orientation': 'all', 'name': 'JohnCleese', 'longitude': -1.553621, 'email': 'foo@example.com', 'phone': '0202020202', 'latitude': 47.218371, 'floor_total': '2', 'floor': 1, 'contrib_type': 'connect', 'connect_local': 'on', } def test_contact_validation(self): no_contact, phone_contact, email_contact, both_contact = [ self.valid_data.copy() for i in range(4)] del phone_contact['email'] del email_contact['phone'] del no_contact['phone'] del no_contact['email'] both_contact.update(phone_contact) both_contact.update(email_contact) self.assertFalse(PublicContribForm(no_contact).is_valid()) self.assertTrue(PublicContribForm(phone_contact).is_valid()) self.assertTrue(PublicContribForm(email_contact).is_valid()) self.assertTrue(PublicContribForm(both_contact).is_valid()) def test_floors_validation(self): invalid_floors = self.valid_data.copy() invalid_floors['floor'] = 2 invalid_floors['floor_total'] = 1 self.assertFalse(PublicContribForm(invalid_floors).is_valid()) self.assertTrue(PublicContribForm(self.valid_data).is_valid()) invalid_floors['floor'] = None invalid_floors['floor_total'] = None self.assertTrue(PublicContribForm(invalid_floors).is_valid()) def test_share_fields_validation(self): data = self.valid_data.copy() data['contrib_type'] = 'share' self.assertFalse(PublicContribForm(data).is_valid()) data['access_type'] = 'cable' self.assertTrue(PublicContribForm(data).is_valid()) @override_settings(NOTIFICATION_EMAILS=['foo@example.com']) def test_add_contrib_like_a_robot(self): robot_data = self.valid_data.copy() robot_data['human_field'] = 'should contain no value' response = self.client.post('/map/contribute', robot_data) self.assertEqual(response.status_code, 403) self.assertEqual(len(mail.outbox), 0) class TestDataImport(TestCase): fixtures = ['bottle_data.yaml'] @classmethod def setUpClass(cls, *args, **kwargs): # Silence the warnings about naive datetimes contained in the yaml. with warnings.catch_warnings(): # Scope warn catch to this block warnings.simplefilter('ignore', RuntimeWarning) return super().setUpClass(*args, **kwargs) def test_re_save(self): for contrib in Contrib.objects.all(): contrib.full_clean() contrib.save() class URLTokenManagerTests(TestCase): def test_sign_unsign_ok(self): input_data = {'foo': 12} at = URLTokenManager().sign(input_data) output_data = URLTokenManager().unsign(at) self.assertEqual(input_data, output_data) def test_sign_unsign_wrong_sig(self): with self.assertRaises(BadSignature): URLTokenManager().unsign( b'eyJmb28iOiAxfTpvUFZ1Q3FsSldtQ2htMXJBMmx5VFV0ZWxDLWM') class ContribTokenManagerTests(TestCase): def test_sign_unsign_ok(self): Contrib.objects.create( name='John2', phone='010101020101', contrib_type=Contrib.CONTRIB_CONNECT, privacy_coordinates=True, latitude=0.1, longitude=0.12, ) contrib = Contrib.objects.all().first() manager = ContribTokenManager() token = manager.mk_token(contrib) self.assertEqual( manager.get_instance_if_allowed(token, contrib.pk), contrib) class TestManagementCommands(TestCase): def setUp(self): contrib = Contrib.objects.create( name='John', email='foo@example.com', contrib_type=Contrib.CONTRIB_CONNECT, latitude=0.5, longitude=0.5, ) contrib.expiration_date = datetime.datetime( 2010, 10, 10, tzinfo=pytz.utc) contrib.save() @override_settings(DATA_EXPIRATION_REMINDERS=[10]) def test_send_expiration_reminders(self): # 11 days before (should not send) with freeze_time('29-09-2010', tz_offset=0): call_command('send_expiration_reminders') self.assertEqual(len(mail.outbox), 0) # 9 days before (should not send) with freeze_time('01-10-2010', tz_offset=0): call_command('send_expiration_reminders') self.assertEqual(len(mail.outbox), 0) # 10 days before (should send) with freeze_time('30-09-2010', tz_offset=0): call_command('send_expiration_reminders', '--dry-run') self.assertEqual(len(mail.outbox), 0) call_command('send_expiration_reminders') self.assertEqual(len(mail.outbox), 1)