diff --git a/api_schemas/user_schemas.py b/api_schemas/user_schemas.py index 04b8d3d..c765755 100644 --- a/api_schemas/user_schemas.py +++ b/api_schemas/user_schemas.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, Annotated, Literal -from pydantic import StringConstraints +from pydantic import StringConstraints, field_validator +from helpers.check_stil_id import check_stil_id from fastapi_users_pelicanq import schemas as fastapi_users_schemas from api_schemas.post_schemas import PostRead from helpers.constants import MAX_FIRST_NAME_LEN, MAX_LAST_NAME_LEN @@ -11,6 +12,19 @@ from api_schemas.group_schema import GroupRead +class StilIdValidationMixin: + stil_id: str | None = None + + @field_validator("stil_id", mode="after") + @classmethod + def validate_stil_id(cls, value: str | None) -> str | None: + if value is None: + return None + if not check_stil_id(value): + raise ValueError("Invalid stil-id") + return value + + class _UserEventRead(BaseSchema): id: int @@ -118,25 +132,25 @@ class UserInGroupRead(BaseSchema): # fastapi-users will take all fields on this model and feed into the user constructor User_DB(...) when /auth/register route is called -class UserCreate(fastapi_users_schemas.BaseUserCreate, BaseSchema): +class UserCreate(StilIdValidationMixin, fastapi_users_schemas.BaseUserCreate, BaseSchema): first_name: Annotated[str, StringConstraints(max_length=MAX_FIRST_NAME_LEN)] last_name: Annotated[str, StringConstraints(max_length=MAX_LAST_NAME_LEN)] telephone_number: PhoneNumber start_year: int | None = None - pass + # stil_id is here, but inherited from StilIdValidationMixin -class UserUpdate(BaseSchema): +class UserUpdate(StilIdValidationMixin, BaseSchema): first_name: str | None = None last_name: str | None = None start_year: int | None = None program: PROGRAM_TYPE | None = None notifications: bool | None = None - stil_id: str | None = None standard_food_preferences: list[str] | None = None other_food_preferences: str | None = None telephone_number: PhoneNumber | None = None moose_game_name: str | None = None + # stil_id is here, but inherited from StilIdValidationMixin class UpdateUserMember(BaseSchema): diff --git a/helpers/check_stil_id.py b/helpers/check_stil_id.py new file mode 100644 index 0000000..3d71214 --- /dev/null +++ b/helpers/check_stil_id.py @@ -0,0 +1,8 @@ +import re + + +def check_stil_id(s: str) -> bool: + if not len(s) == 10: + return False + pattern = r"^[a-z]{2}\d{4}[a-z]{2}-s$" + return bool(re.fullmatch(pattern, s)) diff --git a/services/user.py b/services/user.py index 21759b0..ea7911b 100644 --- a/services/user.py +++ b/services/user.py @@ -4,16 +4,9 @@ from db_models.user_model import User_DB from fastapi import HTTPException, status from sqlalchemy.exc import DataError, NoResultFound, MultipleResultsFound -import re from helpers.types import FOOD_PREFERENCES from db_models.post_model import Post_DB - - -def check_stil_id(s: str) -> bool: - if not len(s) == 10: - return False - pattern = r"^[a-z]{2}\d{4}[a-z]{2}-s$" - return bool(re.fullmatch(pattern, s)) +from helpers.check_stil_id import check_stil_id def condition(model, asset): diff --git a/tests/test_users.py b/tests/test_users.py index 4f6e02a..9118a6b 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -15,6 +15,19 @@ def test_register_user(client, user1_data): assert "id" in data +def test_register_user_invalid_stil_id(client): + """Test user registration with valid data except for an invalid stil_id""" + user_data = user_data_factory( + email="test1@example.com", + last_name="User1", + program="F", + telephone_number="+46701234567", + stil_id="invalid_stil_id", + ) + response = client.post("/auth/register", json=user_data) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + def test_register_duplicate_user(client, user1_data): """Test registration with duplicate email fails""" # Register first user