diff --git a/packages/postgres-database/src/simcore_postgres_database/errors.py b/packages/postgres-database/src/simcore_postgres_database/errors.py index 2577c808089..9c4fb417854 100644 --- a/packages/postgres-database/src/simcore_postgres_database/errors.py +++ b/packages/postgres-database/src/simcore_postgres_database/errors.py @@ -32,23 +32,13 @@ from psycopg2.errors import ( CheckViolation, ForeignKeyViolation, + InvalidTextRepresentation, NotNullViolation, UniqueViolation, ) assert issubclass(UniqueViolation, IntegrityError) # nosec -# TODO: see https://stackoverflow.com/questions/58740043/how-do-i-catch-a-psycopg2-errors-uniqueviolation-error-in-a-python-flask-app -# from sqlalchemy.exc import IntegrityError -# -# from psycopg2.errors import UniqueViolation -# -# try: -# s.commit() -# except IntegrityError as e: -# assert isinstance(e.orig, UniqueViolation) - - __all__: tuple[str, ...] = ( "CheckViolation", "DatabaseError", @@ -58,6 +48,7 @@ "IntegrityError", "InterfaceError", "InternalError", + "InvalidTextRepresentation", "NotNullViolation", "NotSupportedError", "OperationalError", diff --git a/packages/postgres-database/src/simcore_postgres_database/models/users.py b/packages/postgres-database/src/simcore_postgres_database/models/users.py index 5a843094fc6..90d5e063662 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/users.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/users.py @@ -50,19 +50,16 @@ def __lt__(self, other: "UserRole") -> bool: return NotImplemented -class UserStatus(Enum): - """ - pending: user registered but not confirmed - active: user is confirmed and can use the platform - expired: user is not authorized because it expired after a trial period - banned: user is not authorized - deleted: this account is marked for deletion - """ - - CONFIRMATION_PENDING = "PENDING" +class UserStatus(str, Enum): + # This is a transition state. The user is registered but not confirmed. NOTE that state is optional depending on LOGIN_REGISTRATION_CONFIRMATION_REQUIRED + CONFIRMATION_PENDING = "CONFIRMATION_PENDING" + # This user can now operate the platform ACTIVE = "ACTIVE" + # This user is inactive because it expired after a trial period EXPIRED = "EXPIRED" + # This user is inactive because he has been a bad boy BANNED = "BANNED" + # This user is inactive because it was marked for deletion DELETED = "DELETED" diff --git a/packages/postgres-database/tests/conftest.py b/packages/postgres-database/tests/conftest.py index 73c7ec3dae1..c70ec2651c2 100644 --- a/packages/postgres-database/tests/conftest.py +++ b/packages/postgres-database/tests/conftest.py @@ -32,8 +32,8 @@ ) pytest_plugins = [ - "pytest_simcore.repository_paths", "pytest_simcore.pytest_global_environs", + "pytest_simcore.repository_paths", ] diff --git a/packages/postgres-database/tests/test_users.py b/packages/postgres-database/tests/test_users.py index 1a039eebd13..078c964fc52 100644 --- a/packages/postgres-database/tests/test_users.py +++ b/packages/postgres-database/tests/test_users.py @@ -11,7 +11,7 @@ from aiopg.sa.result import ResultProxy, RowProxy from faker import Faker from pytest_simcore.helpers.rawdata_fakers import random_user -from simcore_postgres_database.errors import UniqueViolation +from simcore_postgres_database.errors import InvalidTextRepresentation, UniqueViolation from simcore_postgres_database.models.users import ( _USER_ROLE_TO_LEVEL, UserRole, @@ -101,10 +101,64 @@ def test_user_roles_compares(): @pytest.fixture async def clean_users_db_table(connection: SAConnection): yield - await connection.execute(users.delete()) +async def test_user_status_as_pending( + connection: SAConnection, faker: Faker, clean_users_db_table: None +): + """Checks a bug where the expression + + `user_status = UserStatus(user["status"])` + + raise ValueError because **before** this change `UserStatus.CONFIRMATION_PENDING.value == "PENDING"` + """ + # after changing to UserStatus.CONFIRMATION_PENDING == "CONFIRMATION_PENDING" + with pytest.raises(ValueError): # noqa: PT011 + assert UserStatus("PENDING") == UserStatus.CONFIRMATION_PENDING + + assert UserStatus("CONFIRMATION_PENDING") == UserStatus.CONFIRMATION_PENDING + assert UserStatus.CONFIRMATION_PENDING.value == "CONFIRMATION_PENDING" + assert UserStatus.CONFIRMATION_PENDING == "CONFIRMATION_PENDING" + assert str(UserStatus.CONFIRMATION_PENDING) == "UserStatus.CONFIRMATION_PENDING" + + # tests that the database never stores the word "PENDING" + data = random_user(faker, status="PENDING") + assert data["status"] == "PENDING" + with pytest.raises(InvalidTextRepresentation) as err_info: + await connection.execute(users.insert().values(data)) + + assert 'invalid input value for enum userstatus: "PENDING"' in f"{err_info.value}" + + +@pytest.mark.parametrize( + "status_value", + [ + UserStatus.CONFIRMATION_PENDING, + "CONFIRMATION_PENDING", + ], +) +async def test_user_status_inserted_as_enum_or_int( + status_value: UserStatus | str, + connection: SAConnection, + faker: Faker, + clean_users_db_table: None, +): + # insert as `status_value` + data = random_user(faker, status=status_value) + assert data["status"] == status_value + user_id = await connection.scalar(users.insert().values(data).returning(users.c.id)) + + # get as UserStatus.CONFIRMATION_PENDING + user = await ( + await connection.execute(users.select().where(users.c.id == user_id)) + ).first() + assert user + + assert UserStatus(user.status) == UserStatus.CONFIRMATION_PENDING + assert user.status == UserStatus.CONFIRMATION_PENDING + + async def test_unique_username( connection: SAConnection, faker: Faker, clean_users_db_table: None ):