diff --git a/gratipay/models/participant.py b/gratipay/models/participant.py index 345dc371ab..7df7f417b4 100644 --- a/gratipay/models/participant.py +++ b/gratipay/models/participant.py @@ -549,9 +549,10 @@ def update_email(self, email, confirmed=False): hash_string = self.email.hash if hasattr(self.email,'hash') else '' current_email = self.email.address if hasattr(self.email,'address') else '' ctime = self.email.ctime if hasattr(self.email,'ctime') else utcnow() - if email != current_email: + was_confirmed = self.email.confirmed if hasattr(self.email,'confirmed') else '' + if (email != current_email) or (email == current_email and confirmed == was_confirmed == False): confirmed = False - hash_string = str(uuid.uuid4()) + hash_string = str(uuid.uuid4()) ctime = utcnow() # Send the user an email here with self.db.get_cursor() as c: @@ -560,6 +561,7 @@ def update_email(self, email, confirmed=False): , (email, confirmed, hash_string, ctime,self.username) ) self.set_attributes(email=r) + return r def update_goal(self, goal): typecheck(goal, (Decimal, None)) diff --git a/tests/py/test_email_json.py b/tests/py/test_email_json.py index 7a3fd9c315..3f63566c78 100644 --- a/tests/py/test_email_json.py +++ b/tests/py/test_email_json.py @@ -4,7 +4,7 @@ from gratipay.testing import Harness -class TestMembernameJson(Harness): +class TestEmailJson(Harness): def change_email_address(self, address, user='alice', should_fail=True): self.make_participant("alice") diff --git a/tests/py/test_verify_email_html.py b/tests/py/test_verify_email_html.py new file mode 100644 index 0000000000..77199b5749 --- /dev/null +++ b/tests/py/test_verify_email_html.py @@ -0,0 +1,79 @@ +from gratipay.models.participant import Participant +from gratipay.testing import Harness + + +class TestForVerifyEmail(Harness): + + def change_email_address(self, address, username, should_fail=False): + url = "/%s/email.json" % username + if should_fail: + response = self.client.PxST(url + , {'email': address,} + , auth_as=username + ) + else: + response = self.client.POST(url + , {'email': address,} + , auth_as=username + ) + return response + + def verify_email(self, username, hash_string, should_fail=False): + url = '/%s/verify-email.html?hash=%s' % (username , hash_string) + if should_fail: + response = self.client.GxT(url) + else: + response = self.client.GET(url) + return response + + def test_verify_email_without_adding_email(self): + participant = self.make_participant('alice') + response = self.verify_email(participant.username,'sample-hash', should_fail=True) + assert response.code == 404 + + def test_verify_email_wrong_hash(self): + participant = self.make_participant('alice', claimed_time="now") + self.change_email_address('alice@gmail.com', participant.username) + self.verify_email(participant.username,'sample-hash') + expected = False + actual = Participant.from_username(participant.username).email.confirmed + assert expected == actual + + def test_verify_email(self): + participant = self.make_participant('alice', claimed_time="now") + self.change_email_address('alice@gmail.com', participant.username) + hash_string = Participant.from_username(participant.username).email.hash + self.verify_email(participant.username,hash_string) + expected = True + actual = Participant.from_username(participant.username).email.confirmed + assert expected == actual + + def test_email_is_not_confirmed_after_update(self): + participant = self.make_participant('alice', claimed_time="now") + self.change_email_address('alice@gmail.com', participant.username) + hash_string = Participant.from_username(participant.username).email.hash + self.verify_email(participant.username,hash_string) + self.change_email_address('alice@yahoo.com', participant.username) + expected = False + actual = Participant.from_username(participant.username).email.confirmed + assert expected == actual + + def test_verify_email_after_update(self): + participant = self.make_participant('alice', claimed_time="now") + self.change_email_address('alice@gmail.com', participant.username) + hash_string = Participant.from_username(participant.username).email.hash + self.verify_email(participant.username,hash_string) + self.change_email_address('alice@yahoo.com', participant.username) + hash_string = Participant.from_username(participant.username).email.hash + self.verify_email(participant.username,hash_string) + expected = True + actual = Participant.from_username(participant.username).email.confirmed + assert expected == actual + + def test_hash_is_regenerated_on_update(self): + participant = self.make_participant('alice', claimed_time="now") + self.change_email_address('alice@gmail.com', participant.username) + hash_string_1 = Participant.from_username(participant.username).email.hash + self.change_email_address('alice@gmail.com', participant.username) + hash_string_2 = Participant.from_username(participant.username).email.hash + assert hash_string_1 != hash_string_2 \ No newline at end of file diff --git a/www/%username/verify-email.html.spt b/www/%username/verify-email.html.spt new file mode 100644 index 0000000000..026fbfcadf --- /dev/null +++ b/www/%username/verify-email.html.spt @@ -0,0 +1,53 @@ +"""Verify a participant's email +""" +from gratipay.utils import get_participant +from aspen import Response +from aspen.utils import utcnow +from datetime import timedelta + +[-----------------------------------------------------------------------------] + +participant = get_participant(request, restrict=False) +qs = request.line.uri.querystring +hash_string = qs['hash'] if 'hash' in qs else '' + +if not participant.email: + raise Response(404) + +CONFIRMED = participant.email.confirmed +original_hash = participant.email.hash if hasattr(participant.email, 'hash') else '' +email_ctime = participant.email.ctime if hasattr(participant.email, 'ctime') else '' + +EXPIRED = False + +if not CONFIRMED and hash_string == original_hash: + if utcnow() - email_ctime < timedelta(hours=24): + result = participant.update_email(participant.email.address, True) + CONFIRMED = result.confirmed + else: + EXPIRED = True + +[-----------------------------------------------------------------------------] +{% extends "templates/base.html" %} + +{% block scripts %} + +{% endblock %} + +{% block heading %} +