From cfe2d0ba0e213ea6ec11dea626c536c0c9242dc7 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Sat, 30 Apr 2016 18:37:59 -0400 Subject: [PATCH] implement has_verified_identity --- sql/branch.sql | 32 ++++++++++ tests/py/test_participant_identities.py | 83 +++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/sql/branch.sql b/sql/branch.sql index d4c1cd742e..fbd24f756a 100644 --- a/sql/branch.sql +++ b/sql/branch.sql @@ -36,3 +36,35 @@ CREATE TRIGGER enforce_email_for_participant_identity BEFORE INSERT ON participant_identities FOR EACH ROW EXECUTE PROCEDURE fail_if_no_email(); + + +-- participants.has_verified_identity + +ALTER TABLE participants ADD COLUMN has_verified_identity bool NOT NULL DEFAULT false; + +CREATE FUNCTION update_has_verified_identity() RETURNS trigger AS $$ + BEGIN + UPDATE participants p + SET has_verified_identity=COALESCE(( + SELECT is_verified + FROM participant_identities + WHERE participant_id = OLD.participant_id + AND is_verified + LIMIT 1 + ), false) + WHERE p.id = OLD.participant_id; + RETURN NULL; + END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER propagate_is_verified_changes + AFTER UPDATE OF is_verified ON participant_identities + FOR EACH ROW + EXECUTE PROCEDURE update_has_verified_identity(); + +CREATE TRIGGER propagate_is_verified_removal + AFTER DELETE ON participant_identities + FOR EACH ROW + EXECUTE PROCEDURE update_has_verified_identity(); + +-- We don't need an INSERT trigger, because of the way the defaults play out. diff --git a/tests/py/test_participant_identities.py b/tests/py/test_participant_identities.py index 699d6138ad..d44818b0b0 100644 --- a/tests/py/test_participant_identities.py +++ b/tests/py/test_participant_identities.py @@ -2,11 +2,13 @@ from cryptography.fernet import InvalidToken from gratipay.testing import Harness +from gratipay.models.participant import Participant from gratipay.models.participant.mixins import identity, Identity from gratipay.models.participant.mixins.identity import _validate_info, rekey from gratipay.models.participant.mixins.identity import ParticipantIdentityInfoInvalid from gratipay.models.participant.mixins.identity import ParticipantIdentitySchemaUnknown from gratipay.security.crypto import EncryptingPacker, Fernet +from postgres.orm import ReadOnly from psycopg2 import IntegrityError from pytest import raises @@ -250,6 +252,87 @@ def test_ci_still_logs_an_event_when_noop(self): self.assert_events(self.crusher.id, [None], [self.TTO], ['clear identity']) + # hvi - has_verified_identity + + def test_hvi_defaults_to_false(self): + assert self.crusher.has_verified_identity is False + + def test_hvi_is_read_only(self): + with raises(ReadOnly): + self.crusher.has_verified_identity = True + + def test_hvi_becomes_true_when_an_identity_is_verified(self): + self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {}) + self.crusher.set_identity_verification(self.TTO, True) + assert Participant.from_username('crusher').has_verified_identity + + def test_hvi_becomes_false_when_the_identity_is_unverified(self): + self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {}) + self.crusher.set_identity_verification(self.TTO, True) + self.crusher.set_identity_verification(self.TTO, False) + assert not Participant.from_username('crusher').has_verified_identity + + def test_hvi_stays_true_when_a_secondary_identity_is_verified(self): + self.crusher.store_identity_info(self.USA, 'nothing-enforced', {}) + self.crusher.set_identity_verification(self.USA, True) + self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {}) + self.crusher.set_identity_verification(self.TTO, True) + assert Participant.from_username('crusher').has_verified_identity + + def test_hvi_stays_true_when_the_secondary_identity_is_unverified(self): + self.crusher.store_identity_info(self.USA, 'nothing-enforced', {}) + self.crusher.set_identity_verification(self.USA, True) + self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {}) + self.crusher.set_identity_verification(self.TTO, True) + self.crusher.set_identity_verification(self.TTO, False) + assert Participant.from_username('crusher').has_verified_identity + + def test_hvi_goes_back_to_false_when_both_are_unverified(self): + self.crusher.store_identity_info(self.USA, 'nothing-enforced', {}) + self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {}) + self.crusher.set_identity_verification(self.TTO, True) + self.crusher.set_identity_verification(self.USA, True) + self.crusher.set_identity_verification(self.TTO, False) + self.crusher.set_identity_verification(self.USA, False) + assert not Participant.from_username('crusher').has_verified_identity + + def test_hvi_changes_are_scoped_to_a_participant(self): + self.crusher.store_identity_info(self.USA, 'nothing-enforced', {}) + + bruiser = self.make_participant('bruiser', email_address='bruiser@example.com') + bruiser.store_identity_info(self.USA, 'nothing-enforced', {}) + + self.crusher.set_identity_verification(self.USA, True) + + assert Participant.from_username('crusher').has_verified_identity + assert not Participant.from_username('bruiser').has_verified_identity + + def test_hvi_resets_when_identity_is_cleared(self): + self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {}) + self.crusher.set_identity_verification(self.TTO, True) + self.crusher.clear_identity(self.TTO) + assert not Participant.from_username('crusher').has_verified_identity + + def test_hvi_doesnt_reset_when_penultimate_identity_is_cleared(self): + self.crusher.store_identity_info(self.USA, 'nothing-enforced', {}) + self.crusher.set_identity_verification(self.USA, True) + self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {}) + self.crusher.set_identity_verification(self.TTO, True) + self.crusher.set_identity_verification(self.TTO, False) + self.crusher.clear_identity(self.TTO) + assert Participant.from_username('crusher').has_verified_identity + + def test_hvi_does_reset_when_both_identities_are_cleared(self): + self.crusher.store_identity_info(self.USA, 'nothing-enforced', {}) + self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {}) + self.crusher.set_identity_verification(self.USA, True) + self.crusher.set_identity_verification(self.TTO, True) + self.crusher.set_identity_verification(self.TTO, False) + self.crusher.set_identity_verification(self.USA, False) + self.crusher.clear_identity(self.TTO) + assert not Participant.from_username('crusher').has_verified_identity + + # fine - fail_if_no_email def test_fine_fails_if_no_email(self):