diff --git a/modal_backend/routes/notes.py b/modal_backend/routes/notes.py index 4c535eb..2d73ae5 100644 --- a/modal_backend/routes/notes.py +++ b/modal_backend/routes/notes.py @@ -33,13 +33,13 @@ async def get_notes( type_id: int = Query(None), groups_id: list[int] = Query(None), services_id: list[int] = Query(None), - status: str = Query( + status: ModalStatus | None = Query( enum=["active", "archived"], default=None, ), asc_order: bool = False, - limit: int = 10, - offset: int = 0, + limit: int | None = Query(10, ge=0, description="Лимит записией"), + offset: int | None = Query(0, ge=0, description="Смещение записей на N+offset, где N - первая запись"), user=Depends(UnionAuth()), ) -> list[NoteGet]: """ diff --git a/tests/conftest.py b/tests/conftest.py index c036660..c90e856 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta from functools import lru_cache from pathlib import Path @@ -10,7 +11,14 @@ from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer -from modal_backend.models.db import NoteType +from modal_backend.models.db import Group, ModalStatus, Note, NoteType, Service +from modal_backend.schemas.models import ( + NoteChoicePost, + NoteImagePost, + NoteInfoPost, + NoteRatingPost, + NoteTextPost, +) from modal_backend.settings import Settings @@ -145,15 +153,11 @@ def create_note_type(name: str, type_id: int): def note_types(dbsession): """Создает три разных типа модалок.""" note_type_data = [ - ( - "Name 1", - 1, - ), - ( - "Name 2", - 2, - ), - ("Name 3", 3), + ("info", 1), + ("rating", 2), + ("text", 3), + ("choice", 4), + ("image", 5), ] note_types = [create_note_type(*note_type) for note_type in note_type_data] @@ -165,3 +169,139 @@ def note_types(dbsession): for note_type in note_types: dbsession.delete(note_type) dbsession.commit() + + +def create_group(group_id: int, name: str) -> Group: + """Вспомогательная функция-мини-фабрика для создания разных групп в фикстуре groups.""" + return Group(group_id=group_id, name=name) + + +@pytest.fixture() +def groups(dbsession): + """Создает три группы.""" + group_data = [(1, "Group_1"), (2, "Group_2"), (3, "Group_3")] + groupes = [create_group(*group) for group in group_data] + for group in groupes: + dbsession.add(group) + dbsession.commit() + yield groupes + for group in groupes: + dbsession.delete(group) + dbsession.commit() + + +def create_service(service_id: int, name: str) -> Service: + """Вспомогательная функция-мини-фабрика для создания разных сервисов в фикстуре services.""" + return Service(service_id=service_id, name=name) + + +@pytest.fixture() +def services(dbsession): + """Создает три сервиса.""" + service_data = [(1, "Service_1"), (2, "Service_2"), (3, "Service_3")] + services = [create_service(*service) for service in service_data] + for service in services: + dbsession.add(service) + dbsession.commit() + yield services + for service in services: + dbsession.delete(service) + dbsession.commit() + + +def create_note( + type_id: int, + schema: NoteChoicePost | NoteImagePost | NoteInfoPost | NoteRatingPost | NoteTextPost, + admin_id: int, + status: ModalStatus, +): + return Note( + type_id=type_id, + **schema.model_dump(), + admin_id=admin_id, + status=status, + ) + + +@pytest.fixture() +def notes( + dbsession, + note_types, + groups, + services, + authlib_user_data, +): + note_data = [ + { + "type_id": note_types[0].type_id, + "schema": NoteInfoPost( + header="header_1", + is_always=False, + frequency=10, + group_ids=[group.group_id for group in groups if group.group_id == 3], + service_ids=[service.service_id for service in services if service.service_id == 3], + ), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ACTIVE, + }, + { + "type_id": note_types[1].type_id, + "schema": NoteRatingPost( + header="header_2", + is_always=False, + frequency=10, + group_ids=[group.group_id for group in groups if group.group_id < 3], + service_ids=[service.service_id for service in services if service.service_id < 3], + ), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ACTIVE, + }, + { + "type_id": note_types[2].type_id, + "schema": NoteTextPost( + header="header_3", + is_always=False, + frequency=10, + group_ids=[group.group_id for group in groups if group.group_id == 3], + service_ids=[service.service_id for service in services if service.service_id == 3], + ), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ACTIVE, + }, + { + "type_id": note_types[3].type_id, + "schema": NoteChoicePost( + header="header_4", + is_always=False, + frequency=10, + group_ids=[group.group_id for group in groups if group.group_id < 3], + service_ids=[service.service_id for service in services if service.service_id < 3], + ), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ARCHIVED, + }, + { + "type_id": note_types[4].type_id, + "schema": NoteImagePost( + header="header_5", + is_always=False, + frequency=10, + group_ids=[group.group_id for group in groups if group.group_id == 3], + service_ids=[service.service_id for service in services if service.service_id == 3], + ), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ARCHIVED, + }, + ] + for offset, d in enumerate(note_data): + d["schema"].start_ts = datetime.now() + offset * timedelta(hours=1) + d["schema"].end_ts = datetime.now() + offset * timedelta(hours=1) + + notes = [create_note(**note) for note in note_data] + for note in notes: + dbsession.add(note) + dbsession.commit() + yield notes + for note in notes: + dbsession.delete(note) + dbsession.commit() diff --git a/tests/test_routes/test_groups.py b/tests/test_routes/test_groups.py new file mode 100644 index 0000000..db3ed78 --- /dev/null +++ b/tests/test_routes/test_groups.py @@ -0,0 +1,130 @@ +import pytest +from starlette import status + +from modal_backend.models import Group +from modal_backend.schemas.models import GroupGet +from modal_backend.settings import get_settings + +url: str = "/group" +settings = get_settings() + + +@pytest.mark.parametrize( + "status_code", + [ + (status.HTTP_200_OK), + ], +) +def test_get_group(client, groups, status_code): + response = client.get(url) + assert response.status_code == status_code + + +@pytest.mark.parametrize( + "status_code, body", + [ + (status.HTTP_200_OK, {"group_id": 4, "name": "Group_3"}), + (status.HTTP_409_CONFLICT, {"group_id": 2, "name": "Group_2"}), + (status.HTTP_422_UNPROCESSABLE_CONTENT, {"group_id": "abc", "name": "Group_2"}), + (status.HTTP_422_UNPROCESSABLE_CONTENT, {"group_id": 4, "name": 123}), + ], +) +def test_post_group(client, dbsession, groups, status_code, body): + response = client.post(url, json=body) + assert response.status_code == response.status_code + + if status_code == status.HTTP_200_OK: + response_data = response.json() + response_model = GroupGet(**response_data) + exist_group = dbsession.query(Group).filter(Group.id == response_model.id).one_or_none() + assert exist_group + try: + assert exist_group.group_id == body.get("group_id") + assert exist_group.name == body.get("name") + finally: + dbsession.delete(exist_group) + + +@pytest.mark.parametrize( + "status_code, group_n", + [ + ( + status.HTTP_200_OK, + 1, + ), + ( + status.HTTP_404_NOT_FOUND, + 999, + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + "abc", + ), + ], +) +def test_delete_group(client, dbsession, groups, status_code, group_n): + group_indexes = list(range(len(groups))) + response = client.delete(f"{url}/{groups[group_n].id if group_n in group_indexes else group_n}") + assert response.status_code == status_code + if status_code == status.HTTP_200_OK: + none_exist_group = ( + dbsession.query(Group).filter(Group.id == groups[group_n].id).populate_existing().one_or_none() + ) + assert none_exist_group.is_deleted + + +@pytest.mark.parametrize( + "status_code, body, group_n", + [ + ( + status.HTTP_200_OK, + {"group_id": 999, "name": "New_group_name"}, + 1, + ), + ( + status.HTTP_200_OK, + {"group_id": 2, "name": "New_group_name"}, + 1, + ), + ( + status.HTTP_200_OK, + {"group_id": 999, "name": "Group_2"}, + 1, + ), + ( + status.HTTP_404_NOT_FOUND, + {"group_id": 1, "name": "New_group_name"}, + 999, + ), + ( + status.HTTP_409_CONFLICT, + {"group_id": 2, "name": "Group_2"}, + 1, + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + {"group_id": "abc", "name": "Group_2"}, + 1, + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + {"group_id": 2, "name": 999}, + 1, + ), + ], +) +def test_update_group(client, dbsession, groups, status_code, body, group_n): + group_indexes = list(range(len(groups))) + response = client.patch(f"{url}/{groups[group_n].id if group_n in group_indexes else group_n}", json=body) + assert response.status_code == status_code + + if status_code == status.HTTP_200_OK: + response_data = response.json() + response_model = GroupGet(**response_data) + exist_group = dbsession.query(Group).filter(Group.id == response_model.id).populate_existing().one_or_none() + assert exist_group + try: + assert exist_group.group_id == body.get("group_id") + assert exist_group.name == body.get("name") + finally: + dbsession.delete(exist_group) diff --git a/tests/test_routes/test_note_type.py b/tests/test_routes/test_note_type.py index 4420d16..80e103b 100644 --- a/tests/test_routes/test_note_type.py +++ b/tests/test_routes/test_note_type.py @@ -29,7 +29,7 @@ def test_get_notification_type(client, note_types, status_code): ( status.HTTP_200_OK, { - "type_id": 4, + "type_id": 6, "name": "No_exist_type", }, ), @@ -43,7 +43,7 @@ def test_get_notification_type(client, note_types, status_code): ( status.HTTP_422_UNPROCESSABLE_ENTITY, { - "type_id": 4, + "type_id": 6, "name": 123, }, ), @@ -64,5 +64,8 @@ def test_post_create_notification_type(client, dbsession, note_types, status_cod response_model = NoteTypeGet(**response.json()) exist_note_type = dbsession.query(NoteType).filter(NoteType.type_id == response_model.type_id).one_or_none() assert exist_note_type - assert exist_note_type.name == body.get("name") - assert exist_note_type.type_id == body.get("type_id") + try: + assert exist_note_type.name == body.get("name") + assert exist_note_type.type_id == body.get("type_id") + finally: + dbsession.delete(exist_note_type) diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py new file mode 100644 index 0000000..4de7975 --- /dev/null +++ b/tests/test_routes/test_notes.py @@ -0,0 +1,245 @@ +import pytest +from starlette import status + +from modal_backend.models.db import Note +from modal_backend.settings import get_settings + +url: str = "/notification" +settings = get_settings() + + +@pytest.mark.parametrize( + "status_code, type_id, groups_id, services_id, modal_status, asc_order, limit, offset, len_without_confines", + [ + # позитивные кейсы(объединенные проверки) + ( # все модалки + status.HTTP_200_OK, + None, # type_id + None, # groups_id + None, # services_id + None, # modal_status + None, # asc_order + None, # limit + None, # offset + 5, # len_without_confines + ), + ( # активные - ограничение по лимиту и смещению + порядок + группы + status.HTTP_200_OK, + None, # type_id + [3], # groups_id + [3], # services_id + "active", # modal_status + True, # asc_order + 2, # limit + 1, # offset + 2, # len_without_confines + ), + ( # архив - ограничение по лимиту и смещению + порядок + status.HTTP_200_OK, + None, # type_id + [1, 2, 3], # groups_id + [1, 2, 3], # services_id + "archived", # modal_status + False, # asc_order + 999, # limit + 1, # offset + 2, # len_without_confines + ), + ( # ограничение по группам и сервисам + status.HTTP_404_NOT_FOUND, + None, # type_id + [1, 2], # groups_id + [3], # services_id + None, # modal_status + None, # asc_order + None, # limit + None, # offset + 0, # len_without_confines + ), + ( # ограничение по типу модалки + status.HTTP_200_OK, + 4, # type_id + None, # groups_id + None, # services_id + None, # modal_status + None, # asc_order + None, # limit + None, # offset + 1, # len_without_confines + ), + ( # нулевой лимит + status.HTTP_404_NOT_FOUND, + None, # type_id + None, # groups_id + None, # services_id + None, # modal_status + None, # asc_order + 0, # limit + None, # offset + 0, # len_without_confines + ), + ( # отрицательный лимит + не валидный лимит + status.HTTP_422_UNPROCESSABLE_CONTENT, + None, # type_id + None, # groups_id + None, # services_id + None, # modal_status + None, # asc_order + -1, # limit + None, # offset + 0, # len_without_confines + ), + ( # отрицательное смещение + не валидное смещение + status.HTTP_422_UNPROCESSABLE_CONTENT, + None, # type_id + None, # groups_id + None, # services_id + None, # modal_status + None, # asc_order + None, # limit + -1, # offset + 0, # len_without_confines + ), + ( # offset превышающее lwc и limit + status.HTTP_404_NOT_FOUND, + None, # type_id + None, # groups_id + None, # services_id + None, # modal_status + None, # asc_order + 4, # limit + 999, # offset + 0, # len_without_confines + ), + ( # не существующий type_id + status.HTTP_404_NOT_FOUND, + 999, # type_id + None, # groups_id + None, # services_id + None, # modal_status + None, # asc_order + None, # limit + None, # offset + 0, # len_without_confines + ), + ( # не валидный type_id + status.HTTP_422_UNPROCESSABLE_CONTENT, + "abc", # type_id + None, # groups_id + None, # services_id + None, # modal_status + None, # asc_order + None, # limit + None, # offset + 0, # len_without_confines + ), + ( # не валидные groups_id + status.HTTP_422_UNPROCESSABLE_CONTENT, + None, # type_id + [1, "two", 3], # groups_id + None, # services_id + None, # modal_status + None, # asc_order + None, # limit + None, # offset + 0, # len_without_confines + ), + ( # не валидные service_ids + status.HTTP_422_UNPROCESSABLE_CONTENT, + None, # type_id + None, # groups_id + [1, "two", 3], # services_id + None, # modal_status + None, # asc_order + None, # limit + None, # offset + 0, # len_without_confines + ), + ( # не валидный status + status.HTTP_422_UNPROCESSABLE_CONTENT, + None, # type_id + None, # groups_id + None, # services_id + 999, # modal_status + None, # asc_order + None, # limit + None, # offset + 0, # len_without_confines + ), + ( # не валидные asc_order + status.HTTP_422_UNPROCESSABLE_CONTENT, + None, # type_id + None, # groups_id + None, # services_id + None, # modal_status + 999, # asc_order + None, # limit + None, # offset + 0, # len_without_confines + ), + ], +) +def test_get_notes( + client, + dbsession, + notes, + status_code, + type_id, + groups_id, + services_id, + modal_status, + asc_order, + limit, + offset, + len_without_confines, +): + dict_of_params = { + "type_id": type_id if type_id is not None else None, + "groups_id": groups_id if groups_id is not None else None, + "services_id": services_id if services_id is not None else None, + "status": modal_status if modal_status is not None else None, + "asc_order": asc_order if asc_order is not None else None, + "limit": limit if limit is not None else None, + "offset": offset if offset is not None else None, + } + query = {k: v for k, v in dict_of_params.items() if v is not None} + response = client.get(url, params=query) + + assert response.status_code == status_code + + if status_code == status.HTTP_200_OK: + response_data = response.json() + response_objs_by_id = Note.query(session=dbsession).filter( + Note.id.in_([note.get("id") for note in response_data]) + ) + assert len(response_data) != 0 + + get_limit = query.get("limit", 10) + get_offset = query.get("offset", 0) + # проверка лимита + assert len(response_data) <= get_limit + # проверка смещения и нормальной длины без лимита и без лимита и смещения + if len_without_confines < get_limit: + assert ( + len(response_data) == len_without_confines - get_offset if get_offset < len_without_confines else 0 + ), f"response_data={len(response_data)} != expr={len_without_confines - get_offset if get_offset < len_without_confines else 0}" + elif len_without_confines > get_limit: + assert ( + len(response_data) == get_limit - get_offset if get_offset < get_limit else 0 + ), f"response_data={len(response_data)} != expr={get_limit - get_offset if get_offset < get_limit else 0}" + + # проверяем порядок + check_order = query.get("asc_order", False) + reverse_key = False if check_order else True + + ts_data = sorted([obj.start_ts for obj in response_objs_by_id], reverse=reverse_key) + compare = (lambda x, y: x >= y) if check_order is False else (lambda x, y: x <= y) + assert all(compare(x, y) for x, y in zip(ts_data, ts_data[1:])) + + # проверка корректности данных отфильтрованных модалок + if type_id: + for resp_obj in response_data: + assert resp_obj.get("type_id") == type_id + if modal_status: + for resp_obj in response_data: + assert resp_obj.get("status") == modal_status diff --git a/tests/test_routes/test_services.py b/tests/test_routes/test_services.py new file mode 100644 index 0000000..3b6e9fc --- /dev/null +++ b/tests/test_routes/test_services.py @@ -0,0 +1,132 @@ +import pytest +from starlette import status + +from modal_backend.models import Service +from modal_backend.schemas.models import ServiceGet +from modal_backend.settings import get_settings + +url: str = "/service" +settings = get_settings() + + +@pytest.mark.parametrize( + "status_code", + [ + (status.HTTP_200_OK), + ], +) +def test_get_service(client, services, status_code): + response = client.get(url) + assert response.status_code == status_code + + +@pytest.mark.parametrize( + "status_code, body", + [ + (status.HTTP_200_OK, {"service_id": 4, "name": "Service_3"}), + (status.HTTP_409_CONFLICT, {"service_id": 2, "name": "Service_2"}), + (status.HTTP_422_UNPROCESSABLE_CONTENT, {"service_id": "abc", "name": "Service_2"}), + (status.HTTP_422_UNPROCESSABLE_CONTENT, {"service_id": 4, "name": 123}), + ], +) +def test_post_service(client, dbsession, services, status_code, body): + response = client.post(url, json=body) + assert response.status_code == response.status_code + + if status_code == status.HTTP_200_OK: + response_data = response.json() + response_model = ServiceGet(**response_data) + exist_service = dbsession.query(Service).filter(Service.id == response_model.id).one_or_none() + assert exist_service + try: + assert exist_service.service_id == body.get("service_id") + assert exist_service.name == body.get("name") + finally: + dbsession.delete(exist_service) + + +@pytest.mark.parametrize( + "status_code, service_n", + [ + ( + status.HTTP_200_OK, + 1, + ), + ( + status.HTTP_404_NOT_FOUND, + 999, + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + "abc", + ), + ], +) +def test_delete_service(client, dbsession, services, status_code, service_n): + service_indexes = list(range(len(services))) + response = client.delete(f"{url}/{services[service_n].id if service_n in service_indexes else service_n}") + assert response.status_code == status_code + if status_code == status.HTTP_200_OK: + none_exist_service = ( + dbsession.query(Service).filter(Service.id == services[service_n].id).populate_existing().one_or_none() + ) + assert none_exist_service.is_deleted + + +@pytest.mark.parametrize( + "status_code, body, service_n", + [ + ( + status.HTTP_200_OK, + {"service_id": 999, "name": "New_service_name"}, + 1, + ), + ( + status.HTTP_200_OK, + {"service_id": 2, "name": "New_service_name"}, + 1, + ), + ( + status.HTTP_200_OK, + {"service_id": 999, "name": "Service_2"}, + 1, + ), + ( + status.HTTP_404_NOT_FOUND, + {"service_id": 1, "name": "New_service_name"}, + 999, + ), + ( + status.HTTP_409_CONFLICT, + {"service_id": 2, "name": "Service_2"}, + 1, + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + {"service_id": "abc", "name": "Service_2"}, + 1, + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + {"service_id": 2, "name": 999}, + 1, + ), + ], +) +def test_update_service(client, dbsession, services, status_code, body, service_n): + service_indexes = list(range(len(services))) + response = client.patch(f"{url}/{services[service_n].id if service_n in service_indexes else service_n}", json=body) + assert response.status_code == status_code + + if status_code == status.HTTP_200_OK: + response_data = response.json() + response_model = ServiceGet(**response_data) + exist_service = ( + dbsession.query(Service).filter(Service.id == response_model.id).populate_existing().one_or_none() + ) + assert exist_service + try: + assert exist_service.service_id == body.get("service_id") + assert exist_service.name == body.get("name") + finally: + dbsession.delete(exist_service)