diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ebd12459e..7814e7dc3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,9 +64,10 @@ Install npm packages cd static && npm install ``` -To run the code locally, please create a local setting file based on `example.env`: +To run the code locally, go back to the root folder, and please create a local setting file based on `example.env`: ``` +cd .. cp example.env .env ``` diff --git a/app/api/views/new_random_alias.py b/app/api/views/new_random_alias.py index a2af4b6c6..3bbc69e9f 100644 --- a/app/api/views/new_random_alias.py +++ b/app/api/views/new_random_alias.py @@ -94,12 +94,14 @@ def new_random_alias(): scheme = user.alias_generator mode = request.args.get("mode") if mode: - if mode == "word": - scheme = AliasGeneratorEnum.word.value - elif mode == "uuid": - scheme = AliasGeneratorEnum.uuid.value - else: - return jsonify(error=f"{mode} must be either word or uuid"), 400 + try: + scheme = AliasGeneratorEnum[mode].value + except KeyError: + modes = ", ".join(AliasGeneratorEnum.get_names()) + return ( + jsonify(error=f"{mode} must be one of {modes}"), + 400, + ) alias = Alias.create_new_random(user=user, scheme=scheme, note=note) Session.commit() diff --git a/app/api/views/setting.py b/app/api/views/setting.py index 58e314d60..98fdfe48d 100644 --- a/app/api/views/setting.py +++ b/app/api/views/setting.py @@ -17,9 +17,10 @@ def setting_to_dict(user: User): ret = { "notification": user.notification, - "alias_generator": "word" - if user.alias_generator == AliasGeneratorEnum.word.value - else "uuid", + # return the default alias generator in case user uses a non-supported alias generator + "alias_generator": user.alias_generator + if AliasGeneratorEnum.has_name(user.alias_generator) + else AliasGeneratorEnum.get_name(AliasGeneratorEnum.word.value), "random_alias_default_domain": user.default_random_alias_domain(), # return the default sender format (AT) in case user uses a non-supported sender format "sender_format": SenderFormatEnum.get_name(user.sender_format) @@ -48,7 +49,7 @@ def update_setting(): Update user setting Input: - notification: bool - - alias_generator: word|uuid + - alias_generator: word|uuid|random_string - random_alias_default_domain: str """ user = g.user @@ -59,14 +60,11 @@ def update_setting(): if "alias_generator" in data: alias_generator = data["alias_generator"] - if alias_generator not in ["word", "uuid"]: + try: + user.alias_generator = AliasGeneratorEnum[alias_generator].value + except KeyError: return jsonify(error="Invalid alias_generator"), 400 - if alias_generator == "word": - user.alias_generator = AliasGeneratorEnum.word.value - else: - user.alias_generator = AliasGeneratorEnum.uuid.value - if "sender_format" in data: sender_format = data["sender_format"] if not SenderFormatEnum.has_name(sender_format): diff --git a/app/models.py b/app/models.py index e57a8d08f..08038356a 100644 --- a/app/models.py +++ b/app/models.py @@ -1,9 +1,10 @@ import enum import os import random +import string import uuid from email.utils import formataddr -from typing import List, Tuple, Optional +from typing import List, Set, Tuple, Optional import arrow import sqlalchemy as sa @@ -170,33 +171,35 @@ def __repr__(self): class EnumE(enum.Enum): - @classmethod - def has_value(cls, value: int) -> bool: - return value in set(item.value for item in cls) - @classmethod def get_name(cls, value: int) -> Optional[str]: - for item in cls: - if item.value == value: - return item.name + try: + return cls(value).name + except ValueError: + return None - return None + @classmethod + def get_names(cls) -> List[str]: + return [item.name for item in cls] @classmethod def has_name(cls, name: str) -> bool: - for item in cls: - if item.name == name: - return True - - return False + return not cls.get_value(name) is None @classmethod def get_value(cls, name: str) -> Optional[int]: - for item in cls: - if item.name == name: - return item.value + try: + return cls[name].value + except KeyError: + return None - return None + @classmethod + def get_values(cls) -> List[int]: + return [item.value for item in cls] + + @classmethod + def has_value(cls, value: int) -> bool: + return not cls.get_name(value) is None class PlanEnum(EnumE): @@ -216,6 +219,7 @@ class SenderFormatEnum(EnumE): class AliasGeneratorEnum(EnumE): word = 1 # aliases are generated based on random words uuid = 2 # aliases are generated based on uuid + random_string = 3 # aliases are generated based on a completely random string class AliasSuffixEnum(EnumE): @@ -1188,7 +1192,10 @@ def generate_email( if scheme == AliasGeneratorEnum.uuid.value: name = uuid.uuid4().hex if in_hex else uuid.uuid4().__str__() random_email = name + "@" + alias_domain - else: + elif scheme == AliasGeneratorEnum.random_string.value: + name = "".join(random.choices(string.digits + string.ascii_lowercase, k=9)) + random_email = name + "@" + alias_domain + else: # use word.value as the default just like the original code random_email = random_words() + "@" + alias_domain random_email = random_email.lower().strip() diff --git a/templates/dashboard/setting.html b/templates/dashboard/setting.html index 8e2d0ab21..1a0931986 100644 --- a/templates/dashboard/setting.html +++ b/templates/dashboard/setting.html @@ -266,6 +266,9 @@ + @@ -297,7 +300,7 @@ Random word from our dictionary diff --git a/tests/api/test_new_random_alias.py b/tests/api/test_new_random_alias.py index b8dbbdd7a..4a3ff6ba5 100644 --- a/tests/api/test_new_random_alias.py +++ b/tests/api/test_new_random_alias.py @@ -101,6 +101,32 @@ def test_custom_mode(flask_client): assert ge.note == "test note" +def test_random_string_mode(flask_client): + login(flask_client) + + # without note + r = flask_client.post( + url_for("api.new_random_alias", mode="random_string"), + ) + + assert r.status_code == 201 + # extract the uuid part + alias = r.json["alias"] + random_string_part: str = alias[: len(alias) - len(EMAIL_DOMAIN) - 1] + assert random_string_part.isalnum() + + # with note + r = flask_client.post( + url_for("api.new_random_alias", mode="random_string"), + json={"note": "test note"}, + ) + + assert r.status_code == 201 + alias = r.json["alias"] + ge = Alias.get_by(email=alias) + assert ge.note == "test note" + + def test_out_of_quota(flask_client): user = login(flask_client) user.trial_end = None diff --git a/tests/api/test_setting.py b/tests/api/test_setting.py index 3f545b3d2..b651224c9 100644 --- a/tests/api/test_setting.py +++ b/tests/api/test_setting.py @@ -12,13 +12,17 @@ def test_get_setting(flask_client): r = flask_client.get("/api/setting") assert r.status_code == 200 - assert r.json == { + e_json = { "alias_generator": "word", "notification": True, "random_alias_default_domain": "sl.local", "sender_format": "AT", "random_alias_suffix": "random_string", } + for key in e_json: + # Assert on a per-key basis for better error reporting + assert r.json[key] == e_json[key] + assert len(r.json) == len(e_json) def test_update_settings_notification(flask_client): @@ -41,6 +45,10 @@ def test_update_settings_alias_generator(flask_client): assert r.status_code == 200 assert user.alias_generator == AliasGeneratorEnum.uuid.value + r = flask_client.patch("/api/setting", json={"alias_generator": "random_string"}) + assert r.status_code == 200 + assert user.alias_generator == AliasGeneratorEnum.random_string.value + def test_update_settings_random_alias_default_domain(flask_client): user = login(flask_client) diff --git a/tests/test_models.py b/tests/test_models.py index cc3c6b9fd..63057870f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -8,6 +8,7 @@ from app.db import Session from app.email_utils import parse_full_address from app.models import ( + AliasGeneratorEnum, generate_email, Alias, Contact, @@ -32,6 +33,14 @@ def test_generate_email(flask_client): assert UUID(email_uuid.split("@")[0], version=4) +def test_generate_email_with_random_string(flask_client): + email = generate_email(scheme=AliasGeneratorEnum.random_string.value) + assert email.endswith("@" + EMAIL_DOMAIN) + + email_random_string = generate_email(scheme=3) + assert (email_random_string.split("@")[0]).isalnum() + + def test_profile_picture_url(flask_client): user = create_new_user()