From 145d9a6198b835cec4e265b5a54e9e562d3218ed Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Fri, 24 Jul 2026 00:30:15 +0300 Subject: [PATCH 01/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20routes/group.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 17 ++- tests/test_routes/test_groups.py | 173 +++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 tests/test_routes/test_groups.py diff --git a/tests/conftest.py b/tests/conftest.py index c036660..d38d79c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer -from modal_backend.models.db import NoteType +from modal_backend.models.db import NoteType, Group from modal_backend.settings import Settings @@ -165,3 +165,18 @@ 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: + 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() diff --git a/tests/test_routes/test_groups.py b/tests/test_routes/test_groups.py new file mode 100644 index 0000000..efbd90f --- /dev/null +++ b/tests/test_routes/test_groups.py @@ -0,0 +1,173 @@ +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) + From 6f796480c8af721791dae0538b7fdec79fff5eb7 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Fri, 24 Jul 2026 00:30:54 +0300 Subject: [PATCH 02/13] =?UTF-8?q?Fix=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20note=5Ftype?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_note_type.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_routes/test_note_type.py b/tests/test_routes/test_note_type.py index 4420d16..5b90c7f 100644 --- a/tests/test_routes/test_note_type.py +++ b/tests/test_routes/test_note_type.py @@ -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) From 97e2bee2fe387704638c025af2059e72ed7176f3 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 25 Jul 2026 13:01:42 +0300 Subject: [PATCH 03/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=84=D0=B8=D0=BA=D1=81=D1=82=D1=83=D1=80?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index d38d79c..6754894 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer -from modal_backend.models.db import NoteType, Group +from modal_backend.models.db import NoteType, Group, Service from modal_backend.settings import Settings @@ -167,10 +167,12 @@ def note_types(dbsession): 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: @@ -180,3 +182,22 @@ def groups(dbsession): 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() + From aa85209c010c8822f35339b53e1668a2aaa29728 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 25 Jul 2026 13:02:33 +0300 Subject: [PATCH 04/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20routes/service.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_services.py | 175 +++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 tests/test_routes/test_services.py diff --git a/tests/test_routes/test_services.py b/tests/test_routes/test_services.py new file mode 100644 index 0000000..49396fc --- /dev/null +++ b/tests/test_routes/test_services.py @@ -0,0 +1,175 @@ +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) + From f62c12d3e750aee121511c85dcdcca49b6209b22 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 25 Jul 2026 20:52:24 +0300 Subject: [PATCH 05/13] Formated --- tests/conftest.py | 114 +++++++++++++++-- tests/test_routes/test_groups.py | 189 +++++++++++----------------- tests/test_routes/test_services.py | 193 +++++++++++------------------ 3 files changed, 250 insertions(+), 246 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6754894..e45746a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,14 @@ from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer -from modal_backend.models.db import NoteType, Group, Service +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 +152,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] @@ -166,14 +169,16 @@ def note_types(dbsession): 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")] + 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) @@ -188,10 +193,11 @@ 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")] + 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) @@ -201,3 +207,87 @@ def services(dbsession): dbsession.delete(service) dbsession.commit() + +def create_note( + type_id: int, + header: str, + schema: NoteChoicePost | NoteImagePost | NoteInfoPost | NoteRatingPost | NoteTextPost, + admin_id: int, + status: ModalStatus, + group_ids: list[int], + service_ids: list[int], +): + return Note( + type_id=type_id, + header=header, + **schema.model_dump(), + admim_id=admin_id, + status=status, + group_ids=group_ids, + service_ids=service_ids, + ) + + +@pytest.fixture() +def notes( + dbsession, + note_types, + groupes, + services, + authlib_user_data, +): + note_data = [ + { + "type_id": note_types[0].type_id, + "header": "header_1", + "schema": NoteInfoPost(), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ACTIVE, + "group_ids": [group.type_id for group in groupes], + "servece_ids": [service.type_id for service in services], + }, + { + "type_id": note_types[1].type_id, + "header": "header_2", + "schema": NoteRatingPost(), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ACTIVE, + "group_ids": [group.type_id for group in groupes], + "servece_ids": [service.type_id for service in services], + }, + { + "type_id": note_types[2].type_id, + "header": "header_3", + "schema": NoteTextPost(), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ACTIVE, + "group_ids": [group.type_id for group in groupes], + "servece_ids": [service.type_id for service in services], + }, + { + "type_id": note_types[3].type_id, + "header": "header_4", + "schema": NoteChoicePost(), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ACTIVE, + "group_ids": [group.type_id for group in groupes], + "servece_ids": [service.type_id for service in services], + }, + { + "type_id": note_types[4].type_id, + "header": "header_5", + "schema": NoteImagePost(), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ACTIVE, + "group_ids": [group.type_id for group in groupes], + "servece_ids": [service.type_id for service in services], + }, + ] + 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 index efbd90f..db3ed78 100644 --- a/tests/test_routes/test_groups.py +++ b/tests/test_routes/test_groups.py @@ -8,50 +8,27 @@ url: str = "/group" settings = get_settings() + @pytest.mark.parametrize( - "status_code", - [ - (status.HTTP_200_OK), - ] - ) + "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 - } - ), - ] - ) + "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 @@ -66,95 +43,76 @@ def test_post_group(client, dbsession, groups, status_code, body): 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", - ), - ] - ) + "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() + 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, - ), - ] - ) + "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) @@ -170,4 +128,3 @@ def test_update_group(client, dbsession, groups, status_code, body, group_n): assert exist_group.name == body.get("name") finally: dbsession.delete(exist_group) - diff --git a/tests/test_routes/test_services.py b/tests/test_routes/test_services.py index 49396fc..3b6e9fc 100644 --- a/tests/test_routes/test_services.py +++ b/tests/test_routes/test_services.py @@ -8,50 +8,27 @@ url: str = "/service" settings = get_settings() + @pytest.mark.parametrize( - "status_code", - [ - (status.HTTP_200_OK), - ] - ) + "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 - } - ), - ] - ) + "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 @@ -68,95 +45,74 @@ def test_post_service(client, dbsession, services, status_code, body): 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", - ), - ] - ) + "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() + 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, - ), - ] - ) + "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) @@ -165,11 +121,12 @@ def test_update_service(client, dbsession, services, status_code, body, service_ 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() + 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) - From fc14f9f4c4a23aff99d65d664532768b430f1c3d Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 25 Jul 2026 21:08:29 +0300 Subject: [PATCH 06/13] =?UTF-8?q?=D0=9E=D1=81=D0=BD=D0=BE=D0=B2=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_notes.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/test_routes/test_notes.py diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py new file mode 100644 index 0000000..164e5d1 --- /dev/null +++ b/tests/test_routes/test_notes.py @@ -0,0 +1,20 @@ +import pytest +from starlette import status +from modal_backend.settings import get_settings + + + +url: str = "/notification" +settings = get_settings() + + +@pytest.mark.parametrize( + "status_code, ", + [ + ( + status.HTTTP_200_OK, + ) + ] +) +def test_get_notes(): + pass From d83a533c57aa2f048333a19dace7fbb24ed647ab Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 25 Jul 2026 21:18:44 +0300 Subject: [PATCH 07/13] Cosmetic fix --- tests/test_routes/test_notes.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index 164e5d1..b0a481c 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -1,20 +1,12 @@ import pytest from starlette import status -from modal_backend.settings import get_settings - +from modal_backend.settings import get_settings url: str = "/notification" settings = get_settings() -@pytest.mark.parametrize( - "status_code, ", - [ - ( - status.HTTTP_200_OK, - ) - ] -) +@pytest.mark.parametrize("status_code, ", [(status.HTTP_200_OK,)]) def test_get_notes(): pass From 5ad723d8341b0d6e4c8649a402f3b5aa19173f63 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 25 Jul 2026 21:23:55 +0300 Subject: [PATCH 08/13] fix --- tests/test_routes/test_note_type.py | 4 ++-- tests/test_routes/test_notes.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_routes/test_note_type.py b/tests/test_routes/test_note_type.py index 5b90c7f..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, }, ), diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index b0a481c..f124eaa 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -8,5 +8,5 @@ @pytest.mark.parametrize("status_code, ", [(status.HTTP_200_OK,)]) -def test_get_notes(): +def test_get_notes(status_code): pass From 0adac61116a4935168b66a3357c7c547b3dfc926 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 28 Jul 2026 17:05:52 +0300 Subject: [PATCH 09/13] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D0=B0=20=D1=84=D0=B8=D0=BA=D1=81=D1=82=D1=83?= =?UTF-8?q?=D1=80=D0=B0=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 156 ++++++++++++++++++++++------------------------ 1 file changed, 74 insertions(+), 82 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e45746a..55ff801 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ from functools import lru_cache +from datetime import datetime, timedelta from pathlib import Path import pytest @@ -10,7 +11,6 @@ from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer -from modal_backend.models.db import Group, ModalStatus, Note, NoteType, Service from modal_backend.schemas.models import ( NoteChoicePost, NoteImagePost, @@ -18,6 +18,7 @@ NoteRatingPost, NoteTextPost, ) +from modal_backend.models.db import NoteType, Group, Service, ModalStatus, Note from modal_backend.settings import Settings @@ -152,10 +153,10 @@ def create_note_type(name: str, type_id: int): def note_types(dbsession): """Создает три разных типа модалок.""" note_type_data = [ - ("info", 1), - ("rating", 2), + ("info",1), + ("rating",2), ("text", 3), - ("choice", 4), + ("choice", 4), ("image", 5), ] @@ -169,16 +170,14 @@ def note_types(dbsession): 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")] + 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) @@ -193,11 +192,10 @@ 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")] + 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) @@ -208,82 +206,75 @@ def services(dbsession): dbsession.commit() -def create_note( - type_id: int, - header: str, - schema: NoteChoicePost | NoteImagePost | NoteInfoPost | NoteRatingPost | NoteTextPost, - admin_id: int, - status: ModalStatus, - group_ids: list[int], - service_ids: list[int], -): - return Note( - type_id=type_id, - header=header, - **schema.model_dump(), - admim_id=admin_id, - status=status, - group_ids=group_ids, - service_ids=service_ids, - ) - +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, - groupes, - services, - authlib_user_data, -): +def notes(dbsession, note_types, groups, services, authlib_user_data,): note_data = [ - { - "type_id": note_types[0].type_id, - "header": "header_1", - "schema": NoteInfoPost(), - "admin_id": authlib_user_data.get("id"), - "status": ModalStatus.ACTIVE, - "group_ids": [group.type_id for group in groupes], - "servece_ids": [service.type_id for service in services], - }, - { - "type_id": note_types[1].type_id, - "header": "header_2", - "schema": NoteRatingPost(), - "admin_id": authlib_user_data.get("id"), - "status": ModalStatus.ACTIVE, - "group_ids": [group.type_id for group in groupes], - "servece_ids": [service.type_id for service in services], - }, - { - "type_id": note_types[2].type_id, - "header": "header_3", - "schema": NoteTextPost(), - "admin_id": authlib_user_data.get("id"), - "status": ModalStatus.ACTIVE, - "group_ids": [group.type_id for group in groupes], - "servece_ids": [service.type_id for service in services], - }, - { - "type_id": note_types[3].type_id, - "header": "header_4", - "schema": NoteChoicePost(), - "admin_id": authlib_user_data.get("id"), - "status": ModalStatus.ACTIVE, - "group_ids": [group.type_id for group in groupes], - "servece_ids": [service.type_id for service in services], - }, - { - "type_id": note_types[4].type_id, - "header": "header_5", - "schema": NoteImagePost(), - "admin_id": authlib_user_data.get("id"), - "status": ModalStatus.ACTIVE, - "group_ids": [group.type_id for group in groupes], - "servece_ids": [service.type_id for service in services], - }, - ] - notes = [create_note(**note) for note in 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() @@ -291,3 +282,4 @@ def notes( for note in notes: dbsession.delete(note) dbsession.commit() + From 79c3f305c93ae795aa0b6510c68c26cbe89664a8 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 28 Jul 2026 17:06:47 +0300 Subject: [PATCH 10/13] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D1=83=D1=87=D0=BA=D1=83=20get=5Fnotes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_notes.py | 241 +++++++++++++++++++++++++++++++- 1 file changed, 238 insertions(+), 3 deletions(-) diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index f124eaa..8bd54d8 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -2,11 +2,246 @@ from starlette import status from modal_backend.settings import get_settings +from modal_backend.schemas.models import NoteGet +from modal_backend.models.db import Note url: str = "/notification" settings = get_settings() -@pytest.mark.parametrize("status_code, ", [(status.HTTP_200_OK,)]) -def test_get_notes(status_code): - pass +@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 + + + + + + From ed4deb33e89d977beb29b65658fac517457da20c Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 28 Jul 2026 17:08:14 +0300 Subject: [PATCH 11/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B1=D0=BE=D0=BB=D0=B5=D0=B5=20=D1=82?= =?UTF-8?q?=D0=BE=D1=87=D0=BD=D1=8B=D0=B5=20=D0=B0=D0=BD=D0=BD=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D1=82=D0=B8=D0=BF=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20query-=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B2,=20=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=B1=D0=B5=D0=B6=D0=B0=D1=82=D1=8C=20=D0=BF=D0=B0?= =?UTF-8?q?=D0=BF=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BE=D1=82=D1=80?= =?UTF-8?q?=D0=B8=D1=86=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2=20lim?= =?UTF-8?q?it=20=D0=B8=20offset,=20=D0=B8=20=D0=BD=D0=B5=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=BF=D1=83=D1=81=D0=BA=D0=B0=D1=82=D1=8C=20=D0=BD=D0=B5?= =?UTF-8?q?=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=BD=D1=8B=D0=B5=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=BA=D0=B8=20=D0=B2=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modal_backend/routes/notes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modal_backend/routes/notes.py b/modal_backend/routes/notes.py index 4c535eb..2d36453 100644 --- a/modal_backend/routes/notes.py +++ b/modal_backend/routes/notes.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, Literal from auth_lib.fastapi import UnionAuth from fastapi import APIRouter, Depends, Query @@ -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]: """ From 9416d4b9ad30cf933699971935cffb6d3ca9bdfc Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 28 Jul 2026 17:15:13 +0300 Subject: [PATCH 12/13] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20black=20=D0=B8=20isort=20T=5FT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modal_backend/routes/notes.py | 5 +- tests/conftest.py | 162 ++++++++------- tests/test_routes/test_notes.py | 352 ++++++++++++++++---------------- 3 files changed, 270 insertions(+), 249 deletions(-) diff --git a/modal_backend/routes/notes.py b/modal_backend/routes/notes.py index 2d36453..abed5d2 100644 --- a/modal_backend/routes/notes.py +++ b/modal_backend/routes/notes.py @@ -1,4 +1,4 @@ -from typing import Union, Literal +from typing import Union from auth_lib.fastapi import UnionAuth from fastapi import APIRouter, Depends, Query @@ -33,7 +33,8 @@ async def get_notes( type_id: int = Query(None), groups_id: list[int] = Query(None), services_id: list[int] = Query(None), - status: ModalStatus | None = Query( + status: ModalStatus + | None = Query( enum=["active", "archived"], default=None, ), diff --git a/tests/conftest.py b/tests/conftest.py index 55ff801..c90e856 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ -from functools import lru_cache from datetime import datetime, timedelta +from functools import lru_cache from pathlib import Path import pytest @@ -11,6 +11,7 @@ from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer +from modal_backend.models.db import Group, ModalStatus, Note, NoteType, Service from modal_backend.schemas.models import ( NoteChoicePost, NoteImagePost, @@ -18,7 +19,6 @@ NoteRatingPost, NoteTextPost, ) -from modal_backend.models.db import NoteType, Group, Service, ModalStatus, Note from modal_backend.settings import Settings @@ -153,10 +153,10 @@ def create_note_type(name: str, type_id: int): def note_types(dbsession): """Создает три разных типа модалок.""" note_type_data = [ - ("info",1), - ("rating",2), + ("info", 1), + ("rating", 2), ("text", 3), - ("choice", 4), + ("choice", 4), ("image", 5), ] @@ -170,14 +170,16 @@ def note_types(dbsession): 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")] + 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) @@ -192,10 +194,11 @@ 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")] + 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) @@ -206,75 +209,95 @@ def services(dbsession): 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, - ) +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,): +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, - } - ] + { + "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] + notes = [create_note(**note) for note in note_data] for note in notes: dbsession.add(note) dbsession.commit() @@ -282,4 +305,3 @@ def notes(dbsession, note_types, groups, services, authlib_user_data,): for note in notes: dbsession.delete(note) dbsession.commit() - diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index 8bd54d8..4de7975 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -1,216 +1,217 @@ import pytest from starlette import status -from modal_backend.settings import get_settings -from modal_backend.schemas.models import NoteGet 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_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 + 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 - + 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 - + 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 - + 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 + 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 + 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 + 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 + 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 + ( # 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 + 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 + ( # не существующий 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 + 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 + ( # не валидный 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 + "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 + ( # не валидные 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 + 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 + ( # не валидные 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 + 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 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 + 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 + ( # не валидные 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 + 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} +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])) + 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) @@ -219,11 +220,14 @@ def test_get_notes(client, 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}" + 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}" + 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 @@ -231,7 +235,7 @@ def test_get_notes(client, 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: @@ -239,9 +243,3 @@ def test_get_notes(client, if modal_status: for resp_obj in response_data: assert resp_obj.get("status") == modal_status - - - - - - From 9aef0921f9b9b0c4abf64c30f60d19ec2790ccfc Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 28 Jul 2026 17:31:33 +0300 Subject: [PATCH 13/13] fix --- modal_backend/routes/notes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modal_backend/routes/notes.py b/modal_backend/routes/notes.py index abed5d2..2d73ae5 100644 --- a/modal_backend/routes/notes.py +++ b/modal_backend/routes/notes.py @@ -33,8 +33,7 @@ async def get_notes( type_id: int = Query(None), groups_id: list[int] = Query(None), services_id: list[int] = Query(None), - status: ModalStatus - | None = Query( + status: ModalStatus | None = Query( enum=["active", "archived"], default=None, ),