From 16fb570cfd9ea44898da1ca764e0a4c3368ec4d7 Mon Sep 17 00:00:00 2001 From: Julian Lobbes Date: Thu, 10 Nov 2022 14:58:37 +0100 Subject: [PATCH] feat(usermodel): add User parameter validation methods --- lumi2/usermodel.py | 166 +++++++++++++++++++++++++++++++++++++++- tests/test_usermodel.py | 94 +++++++++++++++++++++++ 2 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 tests/test_usermodel.py diff --git a/lumi2/usermodel.py b/lumi2/usermodel.py index c0dda1c..6b3e418 100644 --- a/lumi2/usermodel.py +++ b/lumi2/usermodel.py @@ -33,18 +33,180 @@ class User: The user's profile picture as a PIL Image object. """ + + @staticmethod + def is_valid_username(input_str: str) -> bool: + """Checks whether the input string is a valid username. + + Valid usernames can contain only uppercase/lowercase latin characters, + numbers, hyphens, underscores and periods. A username must start with a + latin character. + Minimum length is 1, maximum length is 64 characters. + + Parameters + ---------- + input_str : str + The string whose validity as a username to check. + + Returns + ------- + bool + True if input_str is a valid username and False otherwise. + + Raises + ------ + TypeError + If input_str is not of type string. + """ + + if not isinstance(input_str, str): + raise TypeError(f"Expected a string but got: '{type(input_str)}'.") + + # TODO implement + return False + + + @staticmethod + def is_valid_password_hash(input_str: str) -> bool: + """Checks whether the input string is a valid password hash. + + Password hashes are base64-encoded bytes, and the hashing algorithm used + to create the hash is hard to determine, but we can verify that the + provided hash is a non-empty string containing base64-decodeable text. + + Parameters + ---------- + input_str : str + The string whose validity as a password hash to check. + + Returns + ------- + bool + True if input_str is a valid password hash and False otherwise. + + Raises + ------ + TypeError + If input_str is not of type string. + """ + + if not isinstance(input_str, str): + raise TypeError(f"Expected a string but got: '{type(input_str)}'.") + + # TODO implement + return False + + + @staticmethod + def is_valid_email(input_str: str) -> bool: + """Checks whether the input string is a valid email address. + + A valid email address contains no whitespace and at least one '@'-char. + + Parameters + ---------- + input_str : str + The string whose validity as an email address to check. + + Returns + ------- + bool + True if input_str is a valid email address and False otherwise. + + Raises + ------ + TypeError + If input_str is not of type string. + """ + + if not isinstance(input_str, str): + raise TypeError(f"Expected a string but got: '{type(input_str)}'.") + + # TODO implement + return False + + + @staticmethod + def is_valid_person_name(input_str: str) -> bool: + """Checks whether the input string is valid as a first/last/display name. + + Valid names cannot contain whitespace and must be at least one character + long. + + Parameters + ---------- + input_str : str + The string whose validity as a first-/last-/displayname to check. + + Returns + ------- + bool + True if input_str is a valid name and False otherwise. + + Raises + ------ + TypeError + If input_str is not of type string. + """ + + if not isinstance(input_str, str): + raise TypeError(f"Expected a string but got: '{type(input_str)}'.") + + # TODO implement + return False + + + @staticmethod + def is_valid_picture(input_image: Image) -> bool: + """Checks whether the input image is a valid Image object. + + TBD - unsure which formats and filesizes to allow here. + + Parameters + ---------- + input_image : PIL.Image + The Image whose validity to check. + + Returns + ------- + bool + True if input_image is a valid Image and False otherwise. + + Raises + ------ + TypeError + If input_image is not of type PIL.Image. + """ + + if not isinstance(input_image, Image): + raise TypeError(f"Expected a PIL Image but got: '{type(input_image)}'.") + def __init__( self, username: str, password_hash: str, email: str, first_name: str, last_name: str, display_name: str, picture: Image, ): + + if not User.is_valid_username(username): + raise ValueError(f"Not a valid username: '{username}'.") self.username = username + + if not User.is_valid_password_hash(password_hash): + raise ValueError(f"Not a valid password hash: '{password_hash}'.") self.password_hash = password_hash + + if not User.is_valid_email(email): + raise ValueError(f"Not a valid email address: '{email}'.") self.email = email + + for name in [first_name, last_name, display_name]: + if not User.is_valid_person_name(name): + raise ValueError(f"Not a valid name: '{name}'.") self.first_name = first_name self.last_name = last_name self.display_name = display_name - self.picture = picture - # TODO validate params + if not User.is_valid_picture(picture): + raise ValueError(f"Not a valid image: '{picture}'.") + self.picture = picture diff --git a/tests/test_usermodel.py b/tests/test_usermodel.py new file mode 100644 index 0000000..e7db13e --- /dev/null +++ b/tests/test_usermodel.py @@ -0,0 +1,94 @@ +"""Unit tests for the lumi2.usermodel module.""" + +import pytest + +from lumi2.usermodel import User + + +def test_is_valid_username(): + for invalid_type in [None, 0, True, ("x",), ["x"]]: + with pytest.raises(TypeError): + User.is_valid_username(invalid_type) + + invalid_usernames = [ + "", " ", "\u1337", "\t", "\n", + "alice?", "alice=", "alice*", + "1alice", "alice bob", + ] + for username in invalid_usernames: + assert not User.is_valid_username(username) + + valid_usernames = [ + "alice", "Al1ce", + "Aa0-_.", "a", + ] + for username in valid_usernames: + assert User.is_valid_username(username) + + +def test_is_valid_password_hash(): + for invalid_type in [None, 0, True, ("x",), ["x"]]: + with pytest.raises(TypeError): + User.is_valid_password_hash(invalid_type) + + invalid_hashes = [ + "", " ", "\t", "\n", + "foobar===", "ou=foobar", "foobar*", + "foobar$", "foo bar", + ] + for invalid_hash in invalid_hashes: + assert not User.is_valid_password_hash(invalid_hash) + + valid_hashes = [ + # can contain [A-Za-z0-9+/] and up to two '=' at the end + "foobar", "EzM3", + "abcABC123+/=", "a", + ] + for valid_hash in valid_hashes: + assert User.is_valid_password_hash(valid_hash) + + +def test_is_valid_email(): + for invalid_type in [None, 0, True, ("x",), ["x"]]: + with pytest.raises(TypeError): + User.is_valid_email(invalid_type) + + invalid_emails = [ + "", " ", "\t", "\n", + " alice@example.com", "alice", "@", "@.", + "alice@example.com ", "alice@ex ample.com" + ] + for invalid_email in invalid_emails: + assert not User.is_valid_email(invalid_email) + + valid_emails = [ + # can contain [A-Za-z0-9+/] and up to two '=' at the end + "alice@example.com", "a@b.c", "Alice.1337$&@Fo0.xyz", + ] + for valid_email in valid_emails: + assert User.is_valid_email(valid_email) + + +def test_is_valid_person_name(): + for invalid_type in [None, 0, True, ("x",), ["x"]]: + with pytest.raises(TypeError): + User.is_valid_person_name(invalid_type) + + invalid_names = [ + "", " ", "\t", "\n", + "Alice Jones", " Alice", + ] + for invalid_name in invalid_names: + assert not User.is_valid_person_name(invalid_name) + + valid_names = [ + "Alice", "Älice", "Böb", "A1lic3$", + "a", "1", + ] + for valid_name in valid_names: + assert User.is_valid_person_name(valid_name) + + +def test_is_valid_picture(): + # TODO implement + pass