Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions modal_backend/routes/notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
NoteStatus,
NoteTextGet,
NoteTextPost,
NotificationGet,
)
from modal_backend.settings import Settings, get_settings
from modal_backend.utils.services import NoteService
Expand Down Expand Up @@ -98,9 +97,7 @@ async def get_note(


@note.post("/info", response_model=NoteInfoGet)
async def create_note_info(
note: NoteInfoPost, user=Depends(UnionAuth(scopes=["modal.note.create"]))
) -> NotificationGet:
async def create_note_info(note: NoteInfoPost, user=Depends(UnionAuth(scopes=["modal.note.create"]))) -> NoteInfoGet:
"""
Создает новую модалку.

Expand All @@ -110,6 +107,7 @@ async def create_note_info(

Права: `["modal.note.create"]`
"""
await NoteService.validate_note(db, note=note)
new_note = Note.create(
session=db.session,
type_id=NoteTypeEnum.INFO,
Expand All @@ -123,7 +121,7 @@ async def create_note_info(
@note.post("/rating", response_model=NoteRatingGet)
async def create_note_rating(
note: NoteRatingPost, user=Depends(UnionAuth(scopes=["modal.note.create"]))
) -> NotificationGet:
) -> NoteRatingGet:
"""
Создает новую модалку.

Expand All @@ -133,6 +131,7 @@ async def create_note_rating(

Права: `["modal.note.create"]`
"""
await NoteService.validate_note(db, note=note)
new_note = Note.create(
session=db.session,
type_id=NoteTypeEnum.RATING,
Expand All @@ -144,9 +143,7 @@ async def create_note_rating(


@note.post("/text", response_model=NoteTextGet)
async def create_note_text(
note: NoteTextPost, user=Depends(UnionAuth(scopes=["modal.note.create"]))
) -> NotificationGet:
async def create_note_text(note: NoteTextPost, user=Depends(UnionAuth(scopes=["modal.note.create"]))) -> NoteTextGet:
"""
Создает новую модалку.

Expand All @@ -156,6 +153,7 @@ async def create_note_text(

Права: `["modal.note.create"]`
"""
await NoteService.validate_note(db, note=note)
new_note = Note.create(
session=db.session,
type_id=NoteTypeEnum.TEXT,
Expand All @@ -169,7 +167,7 @@ async def create_note_text(
@note.post("/choice", response_model=NoteChoiceGet)
async def create_note_choice(
note: NoteChoicePost, user=Depends(UnionAuth(scopes=["modal.note.create"]))
) -> NotificationGet:
) -> NoteChoiceGet:
"""
Создает новую модалку.

Expand All @@ -179,6 +177,7 @@ async def create_note_choice(

Права: `["modal.note.create"]`
"""
await NoteService.validate_note(db, note=note)
new_note = Note.create(
session=db.session,
type_id=NoteTypeEnum.CHOICE,
Expand All @@ -192,7 +191,7 @@ async def create_note_choice(
@note.post("/image", response_model=NoteImageGet)
async def create_note_images(
note: NoteImagePost, user=Depends(UnionAuth(scopes=["modal.note.create"]))
) -> NotificationGet:
) -> NoteImageGet:
"""
Создает новую модалку.

Expand All @@ -202,6 +201,7 @@ async def create_note_images(

Права: `["modal.note.create"]`
"""
await NoteService.validate_note(db, note=note)
new_note = Note.create(
session=db.session,
type_id=NoteTypeEnum.IMAGE,
Expand Down
2 changes: 1 addition & 1 deletion modal_backend/schemas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class NoteGet(Base):
id: int
type_id: int
header: str
end_ts: datetime.datetime
end_ts: datetime.datetime | None # по тз может быть null, если is_always = true

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

коммент можно удалить
если для меня пр коммент нужен, то его лучше оставлять прям в гитхабе, как я оставляю текущий коммент тебе

status: ModalStatus


Expand Down
49 changes: 47 additions & 2 deletions modal_backend/utils/services.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,63 @@
from datetime import datetime, timezone
from typing import Union

from requests import Session

from modal_backend.exceptions import AlreadyExists, ForbiddenAction, ObjectNotFound
from modal_backend.exceptions import AlreadyExists, ForbiddenAction, ObjectNotFound, ValueError
from modal_backend.models.db import Group, ModalStatus, Note, Service
from modal_backend.schemas.base import StatusResponseModel
from modal_backend.schemas.models import GroupPost, ServicePost
from modal_backend.schemas.models import (
GroupPost,
NoteChoicePost,
NoteImagePost,
NoteInfoPost,
NoteRatingPost,
NoteTextPost,
ServicePost,
)


class NoteService:
"""
Сервис для работы с логикой Notifications и базой данных
"""

@classmethod
async def validate_note(
cls, db: Session, note: Union[NoteInfoPost, NoteRatingPost, NoteTextPost, NoteChoicePost, NoteImagePost]
):
"""Валидация полей при создании note"""
if note.is_always:
note.end_ts = None

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а зачем такая проверка если в схеме прописано что дефолт none

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

и тогда сразу вопрос почему start_ts не делал?) только для конца

elif note.end_ts is None or (note.start_ts is not None and note.start_ts >= note.end_ts):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

лучше просто or все сделать

raise ValueError("Invalid end_ts value")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

нужна кстати проверка что start и end_ts>now иначе я могу сейчас создать модалку которая просрочена

if note.frequency < 1:
raise ValueError("Frequency must be greater than 0")
if not note.group_ids:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

хочется для групп и сервисов сделать проверку на их существование реальное, надо в бд здесь будет обращаться

raise ValueError("Group ids must not be empty")
if not note.service_ids:
raise ValueError("Service ids must not be empty")

if isinstance(note, NoteRatingPost):
if note.rating_max is None or not (2 <= note.rating_max <= 10):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/profcomff/modal-service-api/blob/main/modal_backend/settings.py#L11
хардкор с числом 10 это плохо
надо сделать отдельной переменной для variables в settings и оттуда использовать здесь и в будущих тестах
также как сделаешь переменную надо в github/build_and_publish.yml прописать в джобе эту переменную как ту, которую надо взять из github secrets(с ними я сам разберусь, тебе только желательно в описании жирным пометить что мне надо это сделать)

raise ValueError("Rating max must be between 2 and 10")
elif isinstance(note, NoteChoicePost):
if note.choice_options is None or len(note.choice_options) < 2:
raise ValueError("Choice options must contain at least 2 options")
if len({opt.id for opt in note.choice_options}) != len(note.choice_options):
raise ValueError("Choice options must have unique ids")
if not all(
opt.text for opt in note.choice_options
): # я так понял, что текст из пробелов это тоже текст. Поправьте если нет

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opt.text.strip()
коммент всегда в гитхабе писать, не в коде

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

пробелы в тексте лишние убрать

raise ValueError("Choice options must have non-empty text")
elif isinstance(note, NoteImagePost):
if not note.images:
raise ValueError("Images must not be empty")
elif isinstance(note, NoteTextPost):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

хочется в порядке следования type_id прописать валидацию каждого объекта

if note.max_length is None or not (1 <= note.max_length <= 5000):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

давай для 5000 тоже сделаем как с максимальным рейтингом

raise ValueError("Max length must be between 1 and 5000")

@classmethod
async def get_notes_by_filters(
cls,
Expand Down
81 changes: 74 additions & 7 deletions tests/test_routes/test_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def resolve_items(n_list: list | Any, items: list) -> list | Any:
"header": "string",
"group_ids": [0], # индексы
"service_ids": [0], # индексы
"frequency": 0,
"frequency": 1,
"start_ts": "2026-08-04T17:21:45.694Z",
"end_ts": "2026-08-04T17:22:45.694Z",
"is_always": False,
Expand All @@ -53,11 +53,11 @@ def resolve_items(n_list: list | Any, items: list) -> list | Any:
"header": "string",
"group_ids": [0], # индексы
"service_ids": [0], # индексы
"frequency": 0,
"frequency": 1,
"start_ts": "2026-08-04T17:21:45.694Z",
"end_ts": "2026-08-04T17:22:45.694Z",
"is_always": True,
"rating_max": 0,
"rating_max": 5,
},
NoteRatingGet,
"/rating",
Expand All @@ -68,7 +68,7 @@ def resolve_items(n_list: list | Any, items: list) -> list | Any:
"header": "string",
"group_ids": [0], # индексы
"service_ids": [0], # индексы
"frequency": 0,
"frequency": 1,
"start_ts": "2026-08-04T17:21:45.694Z",
"end_ts": "2026-08-04T17:22:45.694Z",
"is_always": False,
Expand All @@ -84,11 +84,11 @@ def resolve_items(n_list: list | Any, items: list) -> list | Any:
"header": "string",
"group_ids": [0], # индексы
"service_ids": [0], # индексы
"frequency": 0,
"frequency": 1,
"start_ts": "2026-08-04T17:21:45.694Z",
"end_ts": "2026-08-04T17:22:45.694Z",
"is_always": True,
"choice_options": [{"id": 0, "text": "string"}],
"choice_options": [{"id": 0, "text": "string"}, {"id": 1, "text": "string"}],
"is_multiple": True,
},
NoteChoiceGet,
Expand All @@ -100,7 +100,7 @@ def resolve_items(n_list: list | Any, items: list) -> list | Any:
"header": "string",
"group_ids": [0], # индексы
"service_ids": [0], # индексы
"frequency": 0,
"frequency": 1,
"start_ts": "2026-08-04T17:21:45.694Z",
"end_ts": "2026-08-04T17:22:45.694Z",
"is_always": False,
Expand Down Expand Up @@ -211,11 +211,78 @@ def test_create_all_type_of_note(client, dbsession, groups, services, status_cod
assert response_model.status == note.status
assert response_model.admin_id == note.admin_id

if json_body["is_always"]:
assert note.end_ts is None

if type_model is NoteTextGet:
assert len(note.text) <= note.max_length
dbsession.delete(note)


VALID_NOTE_BODIES: dict[str, dict] = {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

все тесты не то что нужно, ты делаешь тесты ради тестов
а надо тесты ради понимания кода
есть предложение исправить все проблемы этого пр по фалидации и потом отдельным сделаешь норм тестики (на созваны объясню что хочу увидеть)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

если что тесты ниже этого коммента удалить надо, те небольшие правки выше правильные

"/info": {"info_text": "string"},
"/rating": {"rating_max": 5},
"/text": {"text": "string", "max_length": 6},
"/choice": {"choice_options": [{"id": 1, "text": "a"}, {"id": 2, "text": "b"}], "is_multiple": False},
"/image": {"images": ["string"]},
}
COMMON_NOTE_BODY: dict = {
"header": "string",
"group_ids": [1],
"service_ids": [1],
"frequency": 1,
"start_ts": "2026-08-04T17:21:45.694Z",
"end_ts": "2026-08-04T17:22:45.694Z",
"is_always": False,
}


@pytest.mark.parametrize(
"path, overrides",
[
pytest.param("/info", {"end_ts": None}, id="end_ts_required_if_not_always"),
pytest.param("/info", {"end_ts": "2026-08-04T17:21:45.694Z"}, id="start_ts_equals_end_ts"),
pytest.param("/info", {"end_ts": "2026-08-04T17:00:00.000Z"}, id="start_ts_after_end_ts"),
pytest.param("/info", {"frequency": 0}, id="frequency_zero"),
pytest.param("/info", {"frequency": -1}, id="frequency_negative"),
pytest.param("/info", {"group_ids": []}, id="group_ids_empty"),
pytest.param("/info", {"group_ids": None}, id="group_ids_null"),
pytest.param("/info", {"service_ids": []}, id="service_ids_empty"),
pytest.param("/info", {"service_ids": None}, id="service_ids_null"),
pytest.param("/rating", {"rating_max": 1}, id="rating_max_below_min"),
pytest.param("/rating", {"rating_max": 11}, id="rating_max_above_max"),
pytest.param("/rating", {"rating_max": None}, id="rating_max_missing"),
pytest.param("/text", {"max_length": 0}, id="max_length_below_min"),
pytest.param("/text", {"max_length": 5001}, id="max_length_above_max"),
pytest.param("/text", {"max_length": None}, id="max_length_missing"),
pytest.param("/choice", {"choice_options": [{"id": 1, "text": "a"}]}, id="choice_options_one_option"),
pytest.param("/choice", {"choice_options": None}, id="choice_options_missing"),
pytest.param(
"/choice", {"choice_options": [{"id": 1, "text": "a"}, {"id": 1, "text": "b"}]}, id="choice_options_same_id"
),
pytest.param(
"/choice",
{"choice_options": [{"id": 1, "text": "a"}, {"id": 2, "text": ""}]},
id="choice_options_empty_text",
),
pytest.param("/image", {"images": []}, id="images_empty"),
pytest.param("/image", {"images": None}, id="images_missing"),
],
)
def test_create_note_validation_error(client, path, overrides):
"""
Нарушение правил валидации из ТЗ даёт 422 в формате StatusResponseModel.
Проверка `status == "Error"` отличает нашу валидацию от стандартной ошибки pydantic (`{"detail": [...]}`).
Ошибки возникают до обращения к БД, поэтому группы и сервисы в БД не нужны.
"""
body = {**COMMON_NOTE_BODY, **VALID_NOTE_BODIES[path], **overrides}

response = client.post(f"{url}{path}", json=body)

assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
assert response.json()["status"] == "Error"


def calculate_expected_len(
all_notes,
type_id: int | None,
Expand Down
Loading