diff options
Diffstat (limited to 'tests/models/thl/test_user.py')
| -rw-r--r-- | tests/models/thl/test_user.py | 688 |
1 files changed, 688 insertions, 0 deletions
diff --git a/tests/models/thl/test_user.py b/tests/models/thl/test_user.py new file mode 100644 index 0000000..4f10861 --- /dev/null +++ b/tests/models/thl/test_user.py @@ -0,0 +1,688 @@ +import json +from datetime import datetime, timezone, timedelta +from decimal import Decimal +from random import randint, choice as rand_choice +from uuid import uuid4 + +import pytest +from pydantic import ValidationError + + +class TestUserUserID: + + def test_valid(self): + from generalresearch.models.thl.user import User + + val = randint(1, 2**30) + user = User(user_id=val) + assert user.user_id == val + + def test_type(self): + from generalresearch.models.thl.user import User + + # It will cast str to int + assert User(user_id="1").user_id == 1 + + # It will cast float to int + assert User(user_id=1.0).user_id == 1 + + # It will cast Decimal to int + assert User(user_id=Decimal("1.0")).user_id == 1 + + # pydantic Validation error is a ValueError, let's check both.. + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=Decimal("1.00000001")) + assert "1 validation error for User" in str(cm.value) + assert "user_id" in str(cm.value) + assert "Input should be a valid integer," in str(cm.value) + + with pytest.raises(expected_exception=ValidationError) as cm: + User(user_id=Decimal("1.00000001")) + assert "1 validation error for User" in str(cm.value) + assert "user_id" in str(cm.value) + assert "Input should be a valid integer," in str(cm.value) + + def test_zero(self): + from generalresearch.models.thl.user import User + + with pytest.raises(expected_exception=ValidationError) as cm: + User(user_id=0) + assert "1 validation error for User" in str(cm.value) + assert "Input should be greater than 0" in str(cm.value) + + def test_negative(self): + from generalresearch.models.thl.user import User + + with pytest.raises(expected_exception=ValidationError) as cm: + User(user_id=-1) + assert "1 validation error for User" in str(cm.value) + assert "Input should be greater than 0" in str(cm.value) + + def test_too_big(self): + from generalresearch.models.thl.user import User + + val = 2**31 + with pytest.raises(expected_exception=ValidationError) as cm: + User(user_id=val) + assert "1 validation error for User" in str(cm.value) + assert "Input should be less than 2147483648" in str(cm.value) + + def test_identifiable(self): + from generalresearch.models.thl.user import User + + val = randint(1, 2**30) + user = User(user_id=val) + assert user.is_identifiable + + +class TestUserProductID: + user_id = randint(1, 2**30) + + def test_valid(self): + from generalresearch.models.thl.user import User + + product_id = uuid4().hex + + user = User(user_id=self.user_id, product_id=product_id) + assert user.user_id == self.user_id + assert user.product_id == product_id + + def test_type(self): + from generalresearch.models.thl.user import User + + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_id=0) + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid string" in str(cm.value) + + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_id=0.0) + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid string" in str(cm.value) + + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_id=Decimal("0")) + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid string" in str(cm.value) + + def test_empty(self): + from generalresearch.models.thl.user import User + + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_id="") + assert "1 validation error for User" in str(cm.value) + assert "String should have at least 32 characters" in str(cm.value) + + def test_invalid_len(self): + from generalresearch.models.thl.user import User + + # Valid uuid4s are 32 char long + product_id = uuid4().hex[:31] + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_id=product_id) + assert "1 validation error for User", str(cm.value) + assert "String should have at least 32 characters", str(cm.value) + + product_id = uuid4().hex * 2 + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, product_id=product_id) + assert "1 validation error for User" in str(cm.value) + assert "String should have at most 32 characters" in str(cm.value) + + product_id = uuid4().hex + product_id *= 2 + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_id=product_id) + assert "1 validation error for User" in str(cm.value) + assert "String should have at most 32 characters" in str(cm.value) + + def test_invalid_uuid(self): + from generalresearch.models.thl.user import User + + # Modify the UUID to break it + product_id = uuid4().hex[:31] + "x" + + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_id=product_id) + assert "1 validation error for User" in str(cm.value) + assert "Invalid UUID" in str(cm.value) + + def test_invalid_hex_form(self): + from generalresearch.models.thl.user import User + + # Sure not in hex form, but it'll get caught for being the + # wrong length before anything else + product_id = str(uuid4()) # '1a93447e-c77b-4cfa-b58e-ed4777d57110' + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_id=product_id) + assert "1 validation error for User" in str(cm.value) + assert "String should have at most 32 characters" in str(cm.value) + + def test_identifiable(self): + """Can't create a User with only a product_id because it also + needs to the product_user_id""" + from generalresearch.models.thl.user import User + + product_id = uuid4().hex + with pytest.raises(expected_exception=ValueError) as cm: + User(product_id=product_id) + assert "1 validation error for User" in str(cm.value) + assert "Value error, User is not identifiable" in str(cm.value) + + +class TestUserProductUserID: + user_id = randint(1, 2**30) + + def randomword(self, length: int = 50): + # Raw so nothing is escaped to add additional backslashes + _bpuid_allowed = r"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&()*+,-.:;<=>?@[]^_{|}~" + return "".join(rand_choice(_bpuid_allowed) for i in range(length)) + + def test_valid(self): + from generalresearch.models.thl.user import User + + product_user_id = uuid4().hex[:12] + user = User(user_id=self.user_id, product_user_id=product_user_id) + + assert user.user_id == self.user_id + assert user.product_user_id == product_user_id + + def test_type(self): + from generalresearch.models.thl.user import User + + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_user_id=0) + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid string" in str(cm.value) + + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_user_id=0.0) + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid string" in str(cm.value) + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, product_user_id=Decimal("0")) + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid string" in str(cm.value) + + def test_empty(self): + from generalresearch.models.thl.user import User + + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_user_id="") + assert "1 validation error for User" in str(cm.value) + assert "String should have at least 3 characters" in str(cm.value) + + def test_invalid_len(self): + from generalresearch.models.thl.user import User + + product_user_id = self.randomword(251) + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_user_id=product_user_id) + assert "1 validation error for User" in str(cm.value) + assert "String should have at most 128 characters" in str(cm.value) + + product_user_id = self.randomword(2) + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_user_id=product_user_id) + assert "1 validation error for User" in str(cm.value) + assert "String should have at least 3 characters" in str(cm.value) + + def test_invalid_chars_space(self): + from generalresearch.models.thl.user import User + + product_user_id = f"{self.randomword(50)} {self.randomword(50)}" + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_user_id=product_user_id) + assert "1 validation error for User" in str(cm.value) + assert "String cannot contain spaces" in str(cm.value) + + def test_invalid_chars_slash(self): + from generalresearch.models.thl.user import User + + product_user_id = f"{self.randomword(50)}\{self.randomword(50)}" + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_user_id=product_user_id) + assert "1 validation error for User" in str(cm.value) + assert "String cannot contain backslash" in str(cm.value) + + product_user_id = f"{self.randomword(50)}/{self.randomword(50)}" + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_user_id=product_user_id) + assert "1 validation error for User" in str(cm.value) + assert "String cannot contain slash" in str(cm.value) + + def test_invalid_chars_backtick(self): + """Yes I could keep doing these specific character checks. However, + I wanted a test that made sure the regex was hit. I do not know + how we want to provide with the level of specific String checks + we do in here for specific error messages.""" + from generalresearch.models.thl.user import User + + product_user_id = f"{self.randomword(50)}`{self.randomword(50)}" + with pytest.raises(expected_exception=ValueError) as cm: + User(user_id=self.user_id, product_user_id=product_user_id) + assert "1 validation error for User" in str(cm.value) + assert "String is not valid regex" in str(cm.value) + + def test_unique_from_product_id(self): + # We removed this filter b/c these users already exist. the manager checks for this + # though and we can't create new users like this + pass + # product_id = uuid4().hex + # + # with pytest.raises(ValueError) as cm: + # User(product_id=product_id, product_user_id=product_id) + # assert "1 validation error for User", str(cm.exception)) + # assert "product_user_id must not equal the product_id", str(cm.exception)) + + def test_identifiable(self): + """Can't create a User with only a product_user_id because it also + needs to the product_id""" + from generalresearch.models.thl.user import User + + product_user_id = uuid4().hex + with pytest.raises(ValueError) as cm: + User(product_user_id=product_user_id) + assert "1 validation error for User" in str(cm.value) + assert "Value error, User is not identifiable" in str(cm.value) + + +class TestUserUUID: + user_id = randint(1, 2**30) + + def test_valid(self): + from generalresearch.models.thl.user import User + + uuid_pk = uuid4().hex + + user = User(user_id=self.user_id, uuid=uuid_pk) + assert user.user_id == self.user_id + assert user.uuid == uuid_pk + + def test_type(self): + from generalresearch.models.thl.user import User + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, uuid=0) + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid string" in str(cm.value) + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, uuid=0.0) + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid string" in str(cm.value) + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, uuid=Decimal("0")) + assert "1 validation error for User", str(cm.value) + assert "Input should be a valid string" in str(cm.value) + + def test_empty(self): + from generalresearch.models.thl.user import User + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, uuid="") + assert "1 validation error for User", str(cm.value) + assert "String should have at least 32 characters", str(cm.value) + + def test_invalid_len(self): + from generalresearch.models.thl.user import User + + # Valid uuid4s are 32 char long + uuid_pk = uuid4().hex[:31] + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, uuid=uuid_pk) + assert "1 validation error for User" in str(cm.value) + assert "String should have at least 32 characters" in str(cm.value) + + # Valid uuid4s are 32 char long + uuid_pk = uuid4().hex + uuid_pk *= 2 + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, uuid=uuid_pk) + assert "1 validation error for User" in str(cm.value) + assert "String should have at most 32 characters" in str(cm.value) + + def test_invalid_uuid(self): + from generalresearch.models.thl.user import User + + # Modify the UUID to break it + uuid_pk = uuid4().hex[:31] + "x" + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, uuid=uuid_pk) + assert "1 validation error for User" in str(cm.value) + assert "Invalid UUID" in str(cm.value) + + def test_invalid_hex_form(self): + from generalresearch.models.thl.user import User + + # Sure not in hex form, but it'll get caught for being the + # wrong length before anything else + uuid_pk = str(uuid4()) # '1a93447e-c77b-4cfa-b58e-ed4777d57110' + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, uuid=uuid_pk) + assert "1 validation error for User" in str(cm.value) + assert "String should have at most 32 characters" in str(cm.value) + + uuid_pk = str(uuid4())[:32] # '1a93447e-c77b-4cfa-b58e-ed4777d57110' + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, uuid=uuid_pk) + assert "1 validation error for User" in str(cm.value) + assert "Invalid UUID" in str(cm.value) + + def test_identifiable(self): + from generalresearch.models.thl.user import User + + user_uuid = uuid4().hex + user = User(uuid=user_uuid) + assert user.is_identifiable + + +class TestUserCreated: + user_id = randint(1, 2**30) + + def test_valid(self): + from generalresearch.models.thl.user import User + + user = User(user_id=self.user_id) + dt = datetime.now(tz=timezone.utc) + user.created = dt + + assert user.created == dt + + def test_tz_naive_throws_init(self): + from generalresearch.models.thl.user import User + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, created=datetime.now(tz=None)) + assert "1 validation error for User" in str(cm.value) + assert "Input should have timezone info" in str(cm.value) + + def test_tz_naive_throws_setter(self): + from generalresearch.models.thl.user import User + + user = User(user_id=self.user_id) + with pytest.raises(ValueError) as cm: + user.created = datetime.now(tz=None) + assert "1 validation error for User" in str(cm.value) + assert "Input should have timezone info" in str(cm.value) + + def test_tz_utc(self): + from generalresearch.models.thl.user import User + + with pytest.raises(ValueError) as cm: + User( + user_id=self.user_id, + created=datetime.now(tz=timezone(-timedelta(hours=8))), + ) + assert "1 validation error for User" in str(cm.value) + assert "Timezone is not UTC" in str(cm.value) + + def test_not_in_future(self): + from generalresearch.models.thl.user import User + + the_future = datetime.now(tz=timezone.utc) + timedelta(minutes=1) + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, created=the_future) + assert "1 validation error for User" in str(cm.value) + assert "Input is in the future" in str(cm.value) + + def test_after_anno_domini(self): + from generalresearch.models.thl.user import User + + before_ad = datetime( + year=2015, month=1, day=1, tzinfo=timezone.utc + ) + timedelta(minutes=1) + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, created=before_ad) + assert "1 validation error for User" in str(cm.value) + assert "Input is before Anno Domini" in str(cm.value) + + +class TestUserLastSeen: + user_id = randint(1, 2**30) + + def test_valid(self): + from generalresearch.models.thl.user import User + + user = User(user_id=self.user_id) + dt = datetime.now(tz=timezone.utc) + user.last_seen = dt + + assert user.last_seen == dt + + def test_tz_naive_throws_init(self): + from generalresearch.models.thl.user import User + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, last_seen=datetime.now(tz=None)) + assert "1 validation error for User" in str(cm.value) + assert "Input should have timezone info" in str(cm.value) + + def test_tz_naive_throws_setter(self): + from generalresearch.models.thl.user import User + + user = User(user_id=self.user_id) + with pytest.raises(ValueError) as cm: + user.last_seen = datetime.now(tz=None) + assert "1 validation error for User" in str(cm.value) + assert "Input should have timezone info" in str(cm.value) + + def test_tz_utc(self): + from generalresearch.models.thl.user import User + + with pytest.raises(ValueError) as cm: + User( + user_id=self.user_id, + last_seen=datetime.now(tz=timezone(-timedelta(hours=8))), + ) + assert "1 validation error for User" in str(cm.value) + assert "Timezone is not UTC" in str(cm.value) + + def test_not_in_future(self): + from generalresearch.models.thl.user import User + + the_future = datetime.now(tz=timezone.utc) + timedelta(minutes=1) + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, last_seen=the_future) + assert "1 validation error for User" in str(cm.value) + assert "Input is in the future" in str(cm.value) + + def test_after_anno_domini(self): + from generalresearch.models.thl.user import User + + before_ad = datetime( + year=2015, month=1, day=1, tzinfo=timezone.utc + ) + timedelta(minutes=1) + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, last_seen=before_ad) + assert "1 validation error for User" in str(cm.value) + assert "Input is before Anno Domini" in str(cm.value) + + +class TestUserBlocked: + user_id = randint(1, 2**30) + + def test_valid(self): + from generalresearch.models.thl.user import User + + user = User(user_id=self.user_id, blocked=True) + assert user.blocked + + def test_str_casting(self): + """We don't want any of these to work, and that's why + we set strict=True on the column""" + from generalresearch.models.thl.user import User + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, blocked="true") + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid boolean" in str(cm.value) + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, blocked="True") + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid boolean" in str(cm.value) + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, blocked="1") + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid boolean" in str(cm.value) + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, blocked="yes") + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid boolean" in str(cm.value) + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, blocked="no") + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid boolean" in str(cm.value) + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, blocked=uuid4().hex) + assert "1 validation error for User" in str(cm.value) + assert "Input should be a valid boolean" in str(cm.value) + + +class TestUserTiming: + user_id = randint(1, 2**30) + + def test_valid(self): + from generalresearch.models.thl.user import User + + created = datetime.now(tz=timezone.utc) - timedelta(minutes=60) + last_seen = datetime.now(tz=timezone.utc) - timedelta(minutes=59) + + user = User(user_id=self.user_id, created=created, last_seen=last_seen) + assert user.created == created + assert user.last_seen == last_seen + + def test_created_first(self): + from generalresearch.models.thl.user import User + + created = datetime.now(tz=timezone.utc) - timedelta(minutes=60) + last_seen = datetime.now(tz=timezone.utc) - timedelta(minutes=59) + + with pytest.raises(ValueError) as cm: + User(user_id=self.user_id, created=last_seen, last_seen=created) + assert "1 validation error for User" in str(cm.value) + assert "User created time invalid" in str(cm.value) + + +class TestUserModelVerification: + """Tests that may be dependent on more than 1 attribute""" + + def test_identifiable(self): + from generalresearch.models.thl.user import User + + product_id = uuid4().hex + product_user_id = uuid4().hex + user = User(product_id=product_id, product_user_id=product_user_id) + assert user.is_identifiable + + def test_valid_helper(self): + from generalresearch.models.thl.user import User + + user_bool = User.is_valid_ubp( + product_id=uuid4().hex, product_user_id=uuid4().hex + ) + assert user_bool + + user_bool = User.is_valid_ubp(product_id=uuid4().hex, product_user_id=" - - - ") + assert not user_bool + + +class TestUserSerialization: + + def test_basic_json(self): + from generalresearch.models.thl.user import User + + product_id = uuid4().hex + product_user_id = uuid4().hex + + user = User( + product_id=product_id, + product_user_id=product_user_id, + created=datetime.now(tz=timezone.utc), + blocked=False, + ) + + d = json.loads(user.to_json()) + assert d.get("product_id") == product_id + assert d.get("product_user_id") == product_user_id + assert not d.get("blocked") + + assert d.get("product") is None + assert d.get("created").endswith("Z") + + def test_basic_dict(self): + from generalresearch.models.thl.user import User + + product_id = uuid4().hex + product_user_id = uuid4().hex + + user = User( + product_id=product_id, + product_user_id=product_user_id, + created=datetime.now(tz=timezone.utc), + blocked=False, + ) + + d = user.to_dict() + assert d.get("product_id") == product_id + assert d.get("product_user_id") == product_user_id + assert not d.get("blocked") + + assert d.get("product") is None + assert d.get("created").tzinfo == timezone.utc + + def test_from_json(self): + from generalresearch.models.thl.user import User + + product_id = uuid4().hex + product_user_id = uuid4().hex + + user = User( + product_id=product_id, + product_user_id=product_user_id, + created=datetime.now(tz=timezone.utc), + blocked=False, + ) + + u = User.model_validate_json(user.to_json()) + assert u.product_id == product_id + assert u.product is None + assert u.created.tzinfo == timezone.utc + + +class TestUserMethods: + + def test_audit_log(self, user, audit_log_manager): + assert user.audit_log is None + user.prefetch_audit_log(audit_log_manager=audit_log_manager) + assert user.audit_log == [] + + audit_log_manager.create_dummy(user_id=user.user_id) + user.prefetch_audit_log(audit_log_manager=audit_log_manager) + assert len(user.audit_log) == 1 + + def test_transactions( + self, user_factory, thl_lm, session_with_tx_factory, product_user_wallet_yes + ): + u1 = user_factory(product=product_user_wallet_yes) + + assert u1.transactions is None + u1.prefetch_transactions(thl_lm=thl_lm) + assert u1.transactions == [] + + session_with_tx_factory(user=u1) + + u1.prefetch_transactions(thl_lm=thl_lm) + assert len(u1.transactions) == 1 + + @pytest.mark.skip(reason="TODO") + def test_location_history(self, user): + assert user.location_history is None |
