From d3b47b89bfc0ec5a2013e8d4889cdf507efd4416 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Fri, 24 Jul 2026 00:30:15 +0300 Subject: [PATCH 01/14] =?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 | 50 +++++++++ tests/test_routes/test_groups.py | 173 +++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 tests/test_routes/test_groups.py diff --git a/tests/conftest.py b/tests/conftest.py index e123f4d..064f4de 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer + from modal_backend.settings import Settings @@ -133,3 +134,52 @@ def client(get_app_with_test_settings, user_mock): app = get_app_with_test_settings client = TestClient(app) return client +<<<<<<< HEAD +======= + + +def create_note_type(name: str, type_id: int): + """Вспомогательная функция-мини-фабрика для создания разных типов модалок в фикстуре note_types.""" + return NoteType(name=name, type_id=type_id) + + +@pytest.fixture() +def note_types(dbsession): + """Создает три разных типа модалок.""" + note_type_data = [ + ( + "Name 1", + 1, + ), + ( + "Name 2", + 2, + ), + ("Name 3", 3), + ] + + note_types = [create_note_type(*note_type) for note_type in note_type_data] + + for note_type in note_types: + dbsession.add(note_type) + dbsession.commit() + yield note_types + 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() +>>>>>>> 66a98ec (Добавлены тесты для routes/group.py) 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 c7ebf1dadb1e57aac9f446605744f1ee65acd57d Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 25 Jul 2026 13:01:42 +0300 Subject: [PATCH 02/14] =?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 | 132 ++++++++++++++++++-- tests/test_routes/test_groups.py | 189 +++++++++++------------------ tests/test_routes/test_notes.py | 12 ++ tests/test_routes/test_services.py | 132 ++++++++++++++++++++ 4 files changed, 338 insertions(+), 127 deletions(-) create mode 100644 tests/test_routes/test_notes.py create mode 100644 tests/test_routes/test_services.py diff --git a/tests/conftest.py b/tests/conftest.py index 064f4de..2466871 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,14 @@ from testcontainers.postgres import PostgresContainer +from modal_backend.models.db import Group, ModalStatus, Note, Service +from modal_backend.schemas.models import ( + NoteChoicePost, + NoteImagePost, + NoteInfoPost, + NoteRatingPost, + NoteTextPost, +) from modal_backend.settings import Settings @@ -147,15 +155,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] @@ -168,12 +172,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) @@ -182,4 +190,106 @@ def groups(dbsession): for group in groupes: dbsession.delete(group) dbsession.commit() ->>>>>>> 66a98ec (Добавлены тесты для routes/group.py) + + +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, + 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_notes.py b/tests/test_routes/test_notes.py new file mode 100644 index 0000000..f124eaa --- /dev/null +++ b/tests/test_routes/test_notes.py @@ -0,0 +1,12 @@ +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.HTTP_200_OK,)]) +def test_get_notes(status_code): + pass 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) From dbedad75ccaff78719df2dfafdd5b9f9895b3ce0 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 28 Jul 2026 17:05:52 +0300 Subject: [PATCH 03/14] =?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 ++++++++++----------- tests/test_routes/test_notes.py | 241 +++++++++++++++++++++++++++++++- 2 files changed, 313 insertions(+), 84 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2466871..d98dcd5 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 @@ -11,7 +12,6 @@ from testcontainers.postgres import PostgresContainer -from modal_backend.models.db import Group, ModalStatus, Note, Service from modal_backend.schemas.models import ( NoteChoicePost, NoteImagePost, @@ -19,6 +19,7 @@ NoteRatingPost, NoteTextPost, ) +from modal_backend.models.db import Group, Service, ModalStatus, Note from modal_backend.settings import Settings @@ -155,10 +156,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), ] @@ -172,16 +173,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) @@ -196,11 +195,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) @@ -210,82 +208,77 @@ 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, - ) + +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() @@ -293,3 +286,4 @@ def notes( 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 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 1e9c1355ef47cb1f498292b563caa0b527a3ad6c Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 28 Jul 2026 17:08:14 +0300 Subject: [PATCH 04/14] =?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 | 6 +- tests/conftest.py | 161 ++++++++------- tests/test_routes/test_notes.py | 352 ++++++++++++++++---------------- 3 files changed, 269 insertions(+), 250 deletions(-) diff --git a/modal_backend/routes/notes.py b/modal_backend/routes/notes.py index 8b56ae5..677853a 100644 --- a/modal_backend/routes/notes.py +++ b/modal_backend/routes/notes.py @@ -34,13 +34,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 d98dcd5..0a0c2ee 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,7 +11,6 @@ from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer - from modal_backend.schemas.models import ( NoteChoicePost, NoteImagePost, @@ -156,10 +155,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), ] @@ -173,14 +172,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) @@ -195,10 +196,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) @@ -209,76 +211,96 @@ 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() @@ -286,4 +308,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 4ca880a8e91d57a43bf48d7372bbf05d926e91b3 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 1 Aug 2026 16:43:30 +0300 Subject: [PATCH 05/14] =?UTF-8?q?=D0=B4=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=BD?= =?UTF-8?q?=D0=B0=20=D1=80=D1=83=D1=87=D0=BA=D1=83=20get=5Fnote?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_notes.py | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index 4de7975..360571a 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -2,6 +2,7 @@ from starlette import status from modal_backend.models.db import Note +from modal_backend.schemas.models import NoteChoiceGet, NoteImageGet, NoteInfoGet, NoteRatingGet, NoteTextGet from modal_backend.settings import get_settings url: str = "/notification" @@ -78,6 +79,7 @@ None, # offset 0, # len_without_confines ), + # негативные кейсы ( # отрицательный лимит + не валидный лимит status.HTTP_422_UNPROCESSABLE_CONTENT, None, # type_id @@ -243,3 +245,54 @@ def test_get_notes( if modal_status: for resp_obj in response_data: assert resp_obj.get("status") == modal_status + + +@pytest.mark.parametrize( + "status_code, note_n, type_model", + [ + ( + status.HTTP_200_OK, + 0, + NoteInfoGet, + ), + ( + status.HTTP_200_OK, + 1, + NoteRatingGet, + ), + ( + status.HTTP_200_OK, + 2, + NoteTextGet, + ), + ( + status.HTTP_200_OK, + 3, + NoteChoiceGet, + ), + ( + status.HTTP_200_OK, + 4, + NoteImageGet, + ), + ( + status.HTTP_404_NOT_FOUND, + -1, + None, + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + "abc", + None, + ), + ], +) +def test_get_note_by_id(client, notes, status_code, note_n, type_model): + notes_indexes = range(len(notes)) + id_of_note = notes[note_n].id if note_n in notes_indexes else note_n + response = client.get(f"{url}/{id_of_note}") + + assert response.status_code == status_code + + if status_code == status.HTTP_200_OK: + type_model.model_validate(response.json(), extra="forbid") From 046baac3e91dae984e51b1a0284e69ffb0a46d40 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 4 Aug 2026 15:59:53 +0300 Subject: [PATCH 06/14] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=84=D0=B8=D0=BA=D1=81=D1=82=D1=83?= =?UTF-8?q?=D1=80=D0=B0=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D1=8E=D1=89=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=BC=D0=BE=D0=B4=D0=B0=D0=BB=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 67 ++++++++--- tests/test_routes/test_notes.py | 203 +++++++++++++++++++++++++------- 2 files changed, 211 insertions(+), 59 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0a0c2ee..ecc57cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from functools import lru_cache from pathlib import Path @@ -182,12 +182,12 @@ def create_group(group_id: int, name: str) -> Group: 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: + groups = [create_group(*group) for group in group_data] + for group in groups: dbsession.add(group) dbsession.commit() - yield groupes - for group in groupes: + yield groups + for group in groups: dbsession.delete(group) dbsession.commit() @@ -234,6 +234,7 @@ def notes( services, authlib_user_data, ): + """Создает 7 модалок: 5 с разными типами, две просроченные.""" note_data = [ { "type_id": note_types[0].type_id, @@ -241,8 +242,8 @@ def notes( 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], + group_ids=[group.id for group in groups][2:3], + service_ids=[service.id for service in services][2:3], ), "admin_id": authlib_user_data.get("id"), "status": ModalStatus.ACTIVE, @@ -253,8 +254,8 @@ def notes( 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], + group_ids=[group.id for group in groups][:2], + service_ids=[service.id for service in services][:2], ), "admin_id": authlib_user_data.get("id"), "status": ModalStatus.ACTIVE, @@ -265,8 +266,8 @@ def notes( 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], + group_ids=[group.id for group in groups][2:3], + service_ids=[service.id for service in services][2:3], ), "admin_id": authlib_user_data.get("id"), "status": ModalStatus.ACTIVE, @@ -277,8 +278,8 @@ def notes( 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], + group_ids=[group.id for group in groups][:2], + service_ids=[service.id for service in services][:2], ), "admin_id": authlib_user_data.get("id"), "status": ModalStatus.ARCHIVED, @@ -289,18 +290,48 @@ def notes( 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], + group_ids=[group.id for group in groups][2:3], + service_ids=[service.id for service in services][2: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) - + d["schema"].start_ts = datetime.now(timezone.utc).replace(tzinfo=None) + offset * timedelta(hours=1) + d["schema"].end_ts = datetime.now(timezone.utc).replace(tzinfo=None) + offset * timedelta(hours=1) + note_data.extend( + [{# просроченная модалка index 5 + "type_id": note_types[4].type_id, + "schema": NoteImagePost( + header="header_6", + is_always=False, + frequency=10, + group_ids=[group.id for group in groups][2:3], + service_ids=[service.id for service in services][2:3], + start_ts = datetime.now(), + end_ts = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1) + ), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ARCHIVED, + }, + {# просроченная модалка c is_always=True index 6 + "type_id": note_types[4].type_id, + "schema": NoteImagePost( + header="header_7", + is_always=True, + frequency=10, + group_ids=[group.id for group in groups][2:3], + service_ids=[service.id for service in services][2:3], + start_ts = datetime.now(), + end_ts = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1) + ), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ARCHIVED, + }] + ) notes = [create_note(**note) for note in note_data] + for note in notes: dbsession.add(note) dbsession.commit() diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index 360571a..02b650f 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -1,8 +1,8 @@ import pytest from starlette import status -from modal_backend.models.db import Note -from modal_backend.schemas.models import NoteChoiceGet, NoteImageGet, NoteInfoGet, NoteRatingGet, NoteTextGet +from modal_backend.models.db import Note, Group, Service, ModalStatus +from modal_backend.schemas.models import NoteInfoGet, NoteRatingGet, NoteTextGet, NoteChoiceGet, NoteImageGet from modal_backend.settings import get_settings url: str = "/notification" @@ -10,25 +10,25 @@ @pytest.mark.parametrize( - "status_code, type_id, groups_id, services_id, modal_status, asc_order, limit, offset, len_without_confines", + "status_code, type_id, group_n_list, service_n_list, modal_status, asc_order, limit, offset, len_without_confines", [ # позитивные кейсы(объединенные проверки) ( # все модалки status.HTTP_200_OK, None, # type_id - None, # groups_id - None, # services_id + None, # group_n_list + None, # service_n_list None, # modal_status None, # asc_order None, # limit None, # offset - 5, # len_without_confines + 7, # len_without_confines ), ( # активные - ограничение по лимиту и смещению + порядок + группы status.HTTP_200_OK, None, # type_id - [3], # groups_id - [3], # services_id + [2], # group_n_list + [2], # service_n_list "active", # modal_status True, # asc_order 2, # limit @@ -38,19 +38,19 @@ ( # архив - ограничение по лимиту и смещению + порядок status.HTTP_200_OK, None, # type_id - [1, 2, 3], # groups_id - [1, 2, 3], # services_id + [0, 1, 2], # group_n_list + [0, 1, 2], # service_n_list "archived", # modal_status False, # asc_order 999, # limit 1, # offset - 2, # len_without_confines + 4, # len_without_confines ), ( # ограничение по группам и сервисам status.HTTP_404_NOT_FOUND, None, # type_id - [1, 2], # groups_id - [3], # services_id + [0, 1], # group_n_list + [2], # service_n_list None, # modal_status None, # asc_order None, # limit @@ -60,8 +60,8 @@ ( # ограничение по типу модалки status.HTTP_200_OK, 4, # type_id - None, # groups_id - None, # services_id + None, # group_n_list + None, # service_n_list None, # modal_status None, # asc_order None, # limit @@ -71,8 +71,8 @@ ( # нулевой лимит status.HTTP_404_NOT_FOUND, None, # type_id - None, # groups_id - None, # services_id + None, # group_n_list + None, # service_n_list None, # modal_status None, # asc_order 0, # limit @@ -83,8 +83,8 @@ ( # отрицательный лимит + не валидный лимит status.HTTP_422_UNPROCESSABLE_CONTENT, None, # type_id - None, # groups_id - None, # services_id + None, # group_n_list + None, # service_n_list None, # modal_status None, # asc_order -1, # limit @@ -94,8 +94,8 @@ ( # отрицательное смещение + не валидное смещение status.HTTP_422_UNPROCESSABLE_CONTENT, None, # type_id - None, # groups_id - None, # services_id + None, # group_n_list + None, # service_n_list None, # modal_status None, # asc_order None, # limit @@ -105,8 +105,8 @@ ( # offset превышающее lwc и limit status.HTTP_404_NOT_FOUND, None, # type_id - None, # groups_id - None, # services_id + None, # group_n_list + None, # service_n_list None, # modal_status None, # asc_order 4, # limit @@ -116,8 +116,8 @@ ( # не существующий type_id status.HTTP_404_NOT_FOUND, 999, # type_id - None, # groups_id - None, # services_id + None, # group_n_list + None, # service_n_list None, # modal_status None, # asc_order None, # limit @@ -127,19 +127,19 @@ ( # не валидный type_id status.HTTP_422_UNPROCESSABLE_CONTENT, "abc", # type_id - None, # groups_id - None, # services_id + None, # group_n_list + None, # service_n_list None, # modal_status None, # asc_order None, # limit None, # offset 0, # len_without_confines ), - ( # не валидные groups_id + ( # не валидные group_n_list status.HTTP_422_UNPROCESSABLE_CONTENT, None, # type_id - [1, "two", 3], # groups_id - None, # services_id + "two", # group_n_list + None, # service_n_list None, # modal_status None, # asc_order None, # limit @@ -149,8 +149,8 @@ ( # не валидные service_ids status.HTTP_422_UNPROCESSABLE_CONTENT, None, # type_id - None, # groups_id - [1, "two", 3], # services_id + None, # group_n_list + "two", # service_n_list None, # modal_status None, # asc_order None, # limit @@ -160,8 +160,8 @@ ( # не валидный status status.HTTP_422_UNPROCESSABLE_CONTENT, None, # type_id - None, # groups_id - None, # services_id + None, # group_n_list + None, # service_n_list 999, # modal_status None, # asc_order None, # limit @@ -171,8 +171,8 @@ ( # не валидные asc_order status.HTTP_422_UNPROCESSABLE_CONTENT, None, # type_id - None, # groups_id - None, # services_id + None, # group_n_list + None, # service_n_list None, # modal_status 999, # asc_order None, # limit @@ -185,16 +185,41 @@ def test_get_notes( client, dbsession, notes, + groups, + services, status_code, type_id, - groups_id, - services_id, + group_n_list, + service_n_list, modal_status, asc_order, limit, offset, len_without_confines, ): + # Добавление id групп и сервисов с проверкой типа, чтобы можно было указать не валидные query-параметры. + group_indexes = range(len(groups)) + service_indexes = range(len(services)) + groups_id = [] + services_id = [] + if isinstance(group_n_list, list): + for group_n in group_n_list: + if group_n in group_indexes: + groups_id.append(groups[group_n].id) + else: + groups_id.append(group_n) + else: + groups_id = group_n_list + + if isinstance(service_n_list, list): + for service_n in service_n_list: + if service_n in service_indexes: + services_id.append(services[service_n].id) + else: + services_id.append(service_n) + else: + services_id = service_n_list + 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, @@ -246,7 +271,6 @@ def test_get_notes( for resp_obj in response_data: assert resp_obj.get("status") == modal_status - @pytest.mark.parametrize( "status_code, note_n, type_model", [ @@ -285,7 +309,8 @@ def test_get_notes( "abc", None, ), - ], + + ] ) def test_get_note_by_id(client, notes, status_code, note_n, type_model): notes_indexes = range(len(notes)) @@ -293,6 +318,102 @@ def test_get_note_by_id(client, notes, status_code, note_n, type_model): response = client.get(f"{url}/{id_of_note}") assert response.status_code == status_code - + if status_code == status.HTTP_200_OK: type_model.model_validate(response.json(), extra="forbid") + + + +@pytest.mark.parametrize( + "status_code, note_n, dict_switch_to", + [ + ( + status.HTTP_200_OK, + 0, + { + "modal_status" : ModalStatus.ARCHIVED, + }, + ), + ( + status.HTTP_404_NOT_FOUND, + -1, + { + }, + ), + ( + status.HTTP_200_OK, + 3, + { + "modal_status" : ModalStatus.ACTIVE, + }, + ), + ( + status.HTTP_403_FORBIDDEN, + 3, + { + "modal_status" : ModalStatus.ACTIVE, + "deleted_service_ids" : True, + }, + ), + ( + status.HTTP_403_FORBIDDEN, + 3, + { + "modal_status" : ModalStatus.ACTIVE, + "deleted_group_ids" : True, + }, + ), + (# просроченная модалка + status.HTTP_403_FORBIDDEN, + 5, + { + "modal_status" : ModalStatus.ACTIVE, + }, + ), + (# просроченная модалка с is_always=True + status.HTTP_403_FORBIDDEN, + 6, + { + "modal_status" : ModalStatus.ACTIVE, + }, + ), + + ] +) +def test_update_note_status(client, dbsession, notes, note_n, status_code, dict_switch_to): + notes_indexes = range(len(notes)) + id_of_note = notes[note_n].id if note_n in notes_indexes else note_n + + if note_n in notes_indexes: + id_of_note = notes[note_n].id + if dict_switch_to.get("deleted_group_ids"): + for group_id in notes[note_n].group_ids: + group = Group.query(session=dbsession).filter(Group.group_id == group_id).first() + dbsession.delete(group) + dbsession.commit() + if dict_switch_to.get("deleted_service_ids"): + for service_id in notes[note_n].service_ids: + service = Service.query(session=dbsession).filter(Service.service_id == service_id).first() + dbsession.delete(service) + dbsession.commit() + + response = client.patch(f"{url}/{id_of_note}/status") + response_data = response.json() + + note = Note.query(session=dbsession).filter(Note.id == response_data.get("id")).populate_existing().first() + match dict_switch_to: + case {"modal_status" : ModalStatus.ARCHIVED}: + assert response.status_code == status_code + if status_code == status.HTTP_200_OK: + assert note.status == ModalStatus.ARCHIVED + case {"modal_status" : ModalStatus.ACTIVE}: + assert response.status_code == status_code + if status_code == status.HTTP_200_OK: + assert note.status == ModalStatus.ACTIVE + case _: + assert response.status_code == status_code + + + + + From abd70429207ac3957c52afd041007c32ed282d64 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 4 Aug 2026 17:09:25 +0300 Subject: [PATCH 07/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=82=D0=B5=D1=81=D1=82=20=D0=BD=D0=B0=20=D1=80?= =?UTF-8?q?=D1=83=D1=87=D0=BA=D1=83=20update=5Fstatus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_notes.py | 90 +++++++++++++++------------------ 1 file changed, 40 insertions(+), 50 deletions(-) diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index 02b650f..0eed32d 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -325,93 +325,83 @@ def test_get_note_by_id(client, notes, status_code, note_n, type_model): @pytest.mark.parametrize( - "status_code, note_n, dict_switch_to", + "status_code, note_n, modal_status, deleted_group_id_flag, deleted_service_id_flag", [ ( status.HTTP_200_OK, 0, - { - "modal_status" : ModalStatus.ARCHIVED, - }, + ModalStatus.ARCHIVED, + False, + False, ), ( status.HTTP_404_NOT_FOUND, -1, - { - }, + ModalStatus.ACTIVE, + False, + False, ), ( status.HTTP_200_OK, 3, - { - "modal_status" : ModalStatus.ACTIVE, - }, + ModalStatus.ACTIVE, + False, + False ), ( status.HTTP_403_FORBIDDEN, 3, - { - "modal_status" : ModalStatus.ACTIVE, - "deleted_service_ids" : True, - }, + ModalStatus.ACTIVE, + True, + False, ), ( status.HTTP_403_FORBIDDEN, 3, - { - "modal_status" : ModalStatus.ACTIVE, - "deleted_group_ids" : True, - }, + ModalStatus.ACTIVE, + False, + True, ), (# просроченная модалка status.HTTP_403_FORBIDDEN, 5, - { - "modal_status" : ModalStatus.ACTIVE, - }, + ModalStatus.ACTIVE, + False, + False, ), (# просроченная модалка с is_always=True - status.HTTP_403_FORBIDDEN, + status.HTTP_200_OK, 6, - { - "modal_status" : ModalStatus.ACTIVE, - }, + ModalStatus.ACTIVE, + False, + False, ), ] ) -def test_update_note_status(client, dbsession, notes, note_n, status_code, dict_switch_to): +def test_update_note_status(client, dbsession, notes, note_n, status_code, modal_status, deleted_group_id_flag, deleted_service_id_flag): + notes_indexes = range(len(notes)) id_of_note = notes[note_n].id if note_n in notes_indexes else note_n - if note_n in notes_indexes: - id_of_note = notes[note_n].id - if dict_switch_to.get("deleted_group_ids"): - for group_id in notes[note_n].group_ids: - group = Group.query(session=dbsession).filter(Group.group_id == group_id).first() - dbsession.delete(group) - dbsession.commit() - if dict_switch_to.get("deleted_service_ids"): - for service_id in notes[note_n].service_ids: - service = Service.query(session=dbsession).filter(Service.service_id == service_id).first() - dbsession.delete(service) - dbsession.commit() + if deleted_group_id_flag: + for group_id in notes[note_n].group_ids: + group = Group.query(session=dbsession).filter(Group.id == group_id).first() + dbsession.delete(group) + dbsession.commit() + if deleted_service_id_flag: + for service_id in notes[note_n].service_ids: + service = Service.query(session=dbsession).filter(Service.id == service_id).first() + dbsession.delete(service) + dbsession.commit() response = client.patch(f"{url}/{id_of_note}/status") - response_data = response.json() + assert response.status_code == status_code - note = Note.query(session=dbsession).filter(Note.id == response_data.get("id")).populate_existing().first() - match dict_switch_to: - case {"modal_status" : ModalStatus.ARCHIVED}: - assert response.status_code == status_code - if status_code == status.HTTP_200_OK: - assert note.status == ModalStatus.ARCHIVED - case {"modal_status" : ModalStatus.ACTIVE}: - assert response.status_code == status_code - if status_code == status.HTTP_200_OK: - assert note.status == ModalStatus.ACTIVE - case _: - assert response.status_code == status_code + if status_code == status.HTTP_200_OK: + response_data = response.json() + note = Note.query(session=dbsession).filter(Note.id == response_data.get("id")).populate_existing().first() + assert note.status == modal_status From b10317d89dba75aac37cdb96ab9136163a66fde3 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 4 Aug 2026 20:19:46 +0300 Subject: [PATCH 08/14] =?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=BD?= =?UTF-8?q?=D0=B0=20=D1=80=D1=83=D1=87=D0=BA=D1=83=20delete=5Fnote?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_notes.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index 0eed32d..b0105fd 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -404,6 +404,20 @@ def test_update_note_status(client, dbsession, notes, note_n, status_code, modal assert note.status == modal_status +@pytest.mark.parametrize( + "status_code, note_n", + [ + (status.HTTP_200_OK, 0), + (status.HTTP_404_NOT_FOUND, -1), + (status.HTTP_422_UNPROCESSABLE_CONTENT, "one"), + ] +) +def test_delete_note(client, dbsession, notes, status_code, note_n): + note_indexes = range(len(notes)) + id_of_note = notes[note_n].id if note_n in note_indexes else note_n - - + response = client.delete(f"{url}/{id_of_note}") + assert response.status_code == status_code + if status_code == status.HTTP_200_OK: + deleted_note = Note.query(session=dbsession).filter(Note.id == id_of_note).populate_existing().one_or_none + assert deleted_note is not None From 958efb37b14b11097ec23e1b933a2b2eda0f0dd8 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 4 Aug 2026 22:01:48 +0300 Subject: [PATCH 09/14] =?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=BD?= =?UTF-8?q?=D0=B0=20=D1=80=D1=83=D1=87=D0=BA=D0=B8=20create=5Fnote=5F*?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 58 ++++---- tests/test_routes/test_notes.py | 244 +++++++++++++++++++++++++++++--- 2 files changed, 251 insertions(+), 51 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ecc57cd..0e138cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -301,34 +301,36 @@ def notes( d["schema"].start_ts = datetime.now(timezone.utc).replace(tzinfo=None) + offset * timedelta(hours=1) d["schema"].end_ts = datetime.now(timezone.utc).replace(tzinfo=None) + offset * timedelta(hours=1) note_data.extend( - [{# просроченная модалка index 5 - "type_id": note_types[4].type_id, - "schema": NoteImagePost( - header="header_6", - is_always=False, - frequency=10, - group_ids=[group.id for group in groups][2:3], - service_ids=[service.id for service in services][2:3], - start_ts = datetime.now(), - end_ts = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1) - ), - "admin_id": authlib_user_data.get("id"), - "status": ModalStatus.ARCHIVED, - }, - {# просроченная модалка c is_always=True index 6 - "type_id": note_types[4].type_id, - "schema": NoteImagePost( - header="header_7", - is_always=True, - frequency=10, - group_ids=[group.id for group in groups][2:3], - service_ids=[service.id for service in services][2:3], - start_ts = datetime.now(), - end_ts = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1) - ), - "admin_id": authlib_user_data.get("id"), - "status": ModalStatus.ARCHIVED, - }] + [ + { # просроченная модалка index 5 + "type_id": note_types[4].type_id, + "schema": NoteImagePost( + header="header_6", + is_always=False, + frequency=10, + group_ids=[group.id for group in groups][2:3], + service_ids=[service.id for service in services][2:3], + start_ts=datetime.now(), + end_ts=datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1), + ), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ARCHIVED, + }, + { # просроченная модалка c is_always=True index 6 + "type_id": note_types[4].type_id, + "schema": NoteImagePost( + header="header_7", + is_always=True, + frequency=10, + group_ids=[group.id for group in groups][2:3], + service_ids=[service.id for service in services][2:3], + start_ts=datetime.now(), + end_ts=datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1), + ), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ARCHIVED, + }, + ] ) notes = [create_note(**note) for note in note_data] diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index b0105fd..c116ab0 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -1,8 +1,8 @@ import pytest from starlette import status -from modal_backend.models.db import Note, Group, Service, ModalStatus -from modal_backend.schemas.models import NoteInfoGet, NoteRatingGet, NoteTextGet, NoteChoiceGet, NoteImageGet +from modal_backend.models.db import Group, ModalStatus, Note, Service +from modal_backend.schemas.models import NoteChoiceGet, NoteImageGet, NoteInfoGet, NoteRatingGet, NoteTextGet from modal_backend.settings import get_settings url: str = "/notification" @@ -202,7 +202,7 @@ def test_get_notes( service_indexes = range(len(services)) groups_id = [] services_id = [] - if isinstance(group_n_list, list): + if isinstance(group_n_list, list): for group_n in group_n_list: if group_n in group_indexes: groups_id.append(groups[group_n].id) @@ -211,7 +211,7 @@ def test_get_notes( else: groups_id = group_n_list - if isinstance(service_n_list, list): + if isinstance(service_n_list, list): for service_n in service_n_list: if service_n in service_indexes: services_id.append(services[service_n].id) @@ -219,7 +219,7 @@ def test_get_notes( services_id.append(service_n) else: services_id = service_n_list - + 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, @@ -271,6 +271,7 @@ def test_get_notes( for resp_obj in response_data: assert resp_obj.get("status") == modal_status + @pytest.mark.parametrize( "status_code, note_n, type_model", [ @@ -309,8 +310,7 @@ def test_get_notes( "abc", None, ), - - ] + ], ) def test_get_note_by_id(client, notes, status_code, note_n, type_model): notes_indexes = range(len(notes)) @@ -318,12 +318,11 @@ def test_get_note_by_id(client, notes, status_code, note_n, type_model): response = client.get(f"{url}/{id_of_note}") assert response.status_code == status_code - + if status_code == status.HTTP_200_OK: type_model.model_validate(response.json(), extra="forbid") - @pytest.mark.parametrize( "status_code, note_n, modal_status, deleted_group_id_flag, deleted_service_id_flag", [ @@ -341,13 +340,7 @@ def test_get_note_by_id(client, notes, status_code, note_n, type_model): False, False, ), - ( - status.HTTP_200_OK, - 3, - ModalStatus.ACTIVE, - False, - False - ), + (status.HTTP_200_OK, 3, ModalStatus.ACTIVE, False, False), ( status.HTTP_403_FORBIDDEN, 3, @@ -362,24 +355,25 @@ def test_get_note_by_id(client, notes, status_code, note_n, type_model): False, True, ), - (# просроченная модалка + ( # просроченная модалка status.HTTP_403_FORBIDDEN, 5, ModalStatus.ACTIVE, False, False, ), - (# просроченная модалка с is_always=True + ( # просроченная модалка с is_always=True status.HTTP_200_OK, 6, ModalStatus.ACTIVE, False, False, ), - - ] + ], ) -def test_update_note_status(client, dbsession, notes, note_n, status_code, modal_status, deleted_group_id_flag, deleted_service_id_flag): +def test_update_note_status( + client, dbsession, notes, note_n, status_code, modal_status, deleted_group_id_flag, deleted_service_id_flag +): notes_indexes = range(len(notes)) id_of_note = notes[note_n].id if note_n in notes_indexes else note_n @@ -410,14 +404,218 @@ def test_update_note_status(client, dbsession, notes, note_n, status_code, modal (status.HTTP_200_OK, 0), (status.HTTP_404_NOT_FOUND, -1), (status.HTTP_422_UNPROCESSABLE_CONTENT, "one"), - ] + ], ) def test_delete_note(client, dbsession, notes, status_code, note_n): note_indexes = range(len(notes)) id_of_note = notes[note_n].id if note_n in note_indexes else note_n - + response = client.delete(f"{url}/{id_of_note}") assert response.status_code == status_code if status_code == status.HTTP_200_OK: deleted_note = Note.query(session=dbsession).filter(Note.id == id_of_note).populate_existing().one_or_none assert deleted_note is not None + + +@pytest.mark.parametrize( + "status_code, body, type_model, path", + [ + ( + status.HTTP_200_OK, + { + "header": "string", + "group_ids": [0], # индексы + "service_ids": [0], # индексы + "frequency": 0, + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": False, + "info_text": "string", + }, + NoteInfoGet, + "/info", + ), + ( + status.HTTP_200_OK, + { + "header": "string", + "group_ids": [0], # индексы + "service_ids": [0], # индексы + "frequency": 0, + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": True, + "rating_max": 0, + }, + NoteRatingGet, + "/rating", + ), + ( + status.HTTP_200_OK, + { + "header": "string", + "group_ids": [0], # индексы + "service_ids": [0], # индексы + "frequency": 0, + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": False, + "text": "string", + "max_length": 6, + }, + NoteTextGet, + "/text", + ), + ( + status.HTTP_200_OK, + { + "header": "string", + "group_ids": [0], # индексы + "service_ids": [0], # индексы + "frequency": 0, + "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"}], + "is_multiple": True, + }, + NoteChoiceGet, + "/choice", + ), + ( + status.HTTP_200_OK, + { + "header": "string", + "group_ids": [0], # индексы + "service_ids": [0], # индексы + "frequency": 0, + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": False, + "images": ["string"], + }, + NoteImageGet, + "/image", + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + { + "header": "string", + "group_ids": "two", # индексы + "service_ids": [1, "two", 3], # индексы + "frequency": "digit", + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": None, + "info_text": "string", + }, + NoteInfoGet, + "/info", + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + { + "header": "string", + "group_ids": "two", # индексы + "service_ids": [1, "two", 3], # индексы + "frequency": "digit", + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": None, + "max_rating": "string", + }, + NoteRatingGet, + "/rating", + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + { + "header": "string", + "group_ids": "two", # индексы + "service_ids": [1, "two", 3], # индексы + "frequency": "digit", + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": None, + "text": "string", + "max_length": "string", + }, + NoteTextGet, + "/text", + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + { + "header": "string", + "group_ids": "two", # индексы + "service_ids": [1, "two", 3], # индексы + "frequency": "digit", + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": None, + "choice_options": [{"id": 0, "text": "string"}], + "is_multiple": None, + }, + NoteChoiceGet, + "/choice", + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + { + "header": "string", + "group_ids": "two", # индексы + "service_ids": [1, "two", 3], # индексы + "frequency": "digit", + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": None, + "images": ["string"], + }, + NoteImageGet, + "/image", + ), + ], +) +def test_create_all_type_of_note(client, dbsession, groups, services, note_types, status_code, body, type_model, path): + group_indexes = range(len(groups)) + service_indexes = range(len(services)) + group_n_list = body.get("group_ids") + service_n_list = body.get("service_ids") + groups_id = [] + services_id = [] + if isinstance(group_n_list, list): + for group_n in group_n_list: + if group_n in group_indexes: + groups_id.append(groups[group_n].id) + else: + groups_id.append(group_n) + else: + groups_id = group_n_list + + if isinstance(service_n_list, list): + for service_n in service_n_list: + if service_n in service_indexes: + services_id.append(services[service_n].id) + else: + services_id.append(service_n) + else: + services_id = service_n_list + + body["group_ids"] = groups_id + body["service_ids"] = services_id + + response = client.post(f"{url}{path}", json=body) + assert response.status_code == status_code + + if status_code == status.HTTP_200_OK: + response_model = type_model.model_validate(response.json(), extra="forbid") + note = Note.query(session=dbsession).filter(Note.id == response_model.id).one_or_none() + assert note + assert response_model.type_id == note.type_id + assert response_model.header == note.header + assert response_model.status == note.status + assert response_model.admin_id == note.admin_id + + if type_model is NoteTextGet: + assert len(note.text) <= note.max_length + dbsession.delete(note) From 99453a3dc1db73483169bdf9a7338bd8cc67ac2c Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Wed, 5 Aug 2026 14:47:02 +0300 Subject: [PATCH 10/14] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B0=D0=BD=D0=BD=D0=BE=D1=82=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=B2=20=D1=80=D1=83=D1=87=D0=BA=D0=B5=20?= =?UTF-8?q?get=5Fnotes=20=D0=B8=20=D0=B2=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modal_backend/routes/notes.py | 6 +++--- modal_backend/utils/services.py | 2 +- tests/test_routes/test_groups.py | 9 +++------ tests/test_routes/test_services.py | 12 ++++-------- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/modal_backend/routes/notes.py b/modal_backend/routes/notes.py index 677853a..7b32245 100644 --- a/modal_backend/routes/notes.py +++ b/modal_backend/routes/notes.py @@ -34,13 +34,13 @@ 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 = Query( enum=["active", "archived"], default=None, ), asc_order: bool = False, - limit: int | None = Query(10, ge=0, description="Лимит записией"), - offset: int | None = Query(0, ge=0, description="Смещение записей на N+offset, где N - первая запись"), + limit: int = Query(10, ge=0, description="Лимит записей"), + offset: int = Query(0, ge=0, description="Смещение записей на N+offset, где N - первая запись"), user=Depends(UnionAuth()), ) -> list[NoteGet]: """ diff --git a/modal_backend/utils/services.py b/modal_backend/utils/services.py index a4b1a05..f77c8b0 100644 --- a/modal_backend/utils/services.py +++ b/modal_backend/utils/services.py @@ -23,7 +23,7 @@ async def get_notes_by_filters( type_id: int = None, groups_id: list[int] = None, services_id: list[int] = None, - status: str = None, + status: ModalStatus | None = None, ): notes_query = ( Note.query(session=db.session) diff --git a/tests/test_routes/test_groups.py b/tests/test_routes/test_groups.py index db3ed78..f41207d 100644 --- a/tests/test_routes/test_groups.py +++ b/tests/test_routes/test_groups.py @@ -15,7 +15,7 @@ (status.HTTP_200_OK), ], ) -def test_get_group(client, groups, status_code): +def test_get_groups(client, groups, status_code): response = client.get(url) assert response.status_code == status_code @@ -123,8 +123,5 @@ def test_update_group(client, dbsession, groups, status_code, body, group_n): 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) + assert exist_group.group_id == body.get("group_id") + assert exist_group.name == body.get("name") diff --git a/tests/test_routes/test_services.py b/tests/test_routes/test_services.py index 3b6e9fc..c6031ac 100644 --- a/tests/test_routes/test_services.py +++ b/tests/test_routes/test_services.py @@ -15,7 +15,7 @@ (status.HTTP_200_OK), ], ) -def test_get_service(client, services, status_code): +def test_get_services(client, services, status_code): response = client.get(url) assert response.status_code == status_code @@ -119,14 +119,10 @@ def test_update_service(client, dbsession, services, status_code, body, service_ assert response.status_code == status_code if status_code == status.HTTP_200_OK: - response_data = response.json() - response_model = ServiceGet(**response_data) + response_model = ServiceGet.model_validate(response.json(), extra="forbid") 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) + assert exist_service.service_id == body.get("service_id") + assert exist_service.name == body.get("name") From fbd3aea3066846db6b728d8d68326fa7a13fe976 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Mon, 10 Aug 2026 00:14:43 +0300 Subject: [PATCH 11/14] =?UTF-8?q?1.=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D0=BC=20=D0=BD=D0=B0=20Git?= =?UTF-8?q?Hub:=20=20=20=20=20-=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B2=20=D1=81=D0=B8=D0=B3=D0=BD=D0=B0=D1=82=D1=83=D1=80?= =?UTF-8?q?=D0=B5=20=D1=80=D1=83=D1=87=D0=BA=D0=B8=20get=5Fnotes=20=D0=B8?= =?UTF-8?q?=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=81=D0=B8=D0=B3=D0=BD=D0=B0=D1=82=D1=83=D1=80=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=B0,=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B9=20=D1=8D=D1=82=D1=83?= =?UTF-8?q?=20=D1=80=D1=83=D1=87=D0=BA=D1=83=20=D0=B2=D1=8B=D0=B7=D1=8B?= =?UTF-8?q?=D0=B2=D0=B0=D0=B5=D1=82=20=20=20=20=20-=20=D0=98=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BD=D0=B5=D0=B9=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD=D0=B3=20=D0=BD=D0=B5=D0=BA=D0=BE=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D1=8B=D1=85=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2(get=5Fgroup?= =?UTF-8?q?s,=20get=5Fservices)=20=20=20=20=20-=20=D0=92=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=85=20=D0=BD=D0=B0=20=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=BF=D0=BF=D1=8B,=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D1=8B=20=D0=B8=20note=5Ftype,=20=D1=81=D0=BF=D0=BE=D1=81=D0=BE?= =?UTF-8?q?=D0=B1=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=20=D1=81=20dbsession.delete=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BA=D0=B0=D1=81=D1=82=D0=BE=D0=BC=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4(=D0=BA=D1=80=D0=BE?= =?UTF-8?q?=D0=BC=D0=B5=20=D0=BC=D0=B5=D1=81=D1=82,=20=D0=B3=D0=B4=D0=B5?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D1=8F=D0=B5=D1=82=D1=81?= =?UTF-8?q?=D1=8F=20=D0=B0=D1=82=D1=80=D0=B8=D0=B1=D1=83=D1=82=20is=5Fdele?= =?UTF-8?q?ted)=20=20=20=20=20-=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=B2=20=D0=BD=D0=B5=D0=BA=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D1=8B=D1=85=20=D1=82=D0=B5=D1=81=D1=82=D0=B0=D1=85?= =?UTF-8?q?,=20=D0=B4=D0=BB=D1=8F=20=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=B9?= =?UTF-8?q?=20=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D0=BE=D1=81=D1=82=D0=B8?= =?UTF-8?q?=20=20=20=20=20-=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BA=D1=80=D0=B8=D1=82=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=D1=8F=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=B2=20=D1=82=D0=B5=D1=81=D1=82=D0=B0=D1=85=20test=5Fgroup?= =?UTF-8?q?s::test=5Fpost=5Fgroup=20=D0=B8=20test=5Fservices::test=5Fpost?= =?UTF-8?q?=5Fservice=20=D1=81=20=D0=B2=D1=81=D0=B5=D0=B3=D0=B4=D0=B0=20?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=B8=D0=BD=D0=BD=D1=8B=D0=BC=20=D0=B0=D1=81?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D1=82=D0=BE=D0=BC=20=20=20=20=20-=20=D0=92?= =?UTF-8?q?=D1=81=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D1=83=D0=BA=D1=82=D1=83=D1=80=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D1=8B=20=D0=BF=D0=BE=20=D0=BC=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=20CRUD=202.=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D0=BC=20=D0=BA=D0=BB=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0=20=20=20=20=20-=20=D0=9F=D1=83=D0=BD=D0=BA=D1=82?= =?UTF-8?q?=D1=8B=203,=206,=207,=208,=209=20(test=5Fget=5Fnotes)=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20-=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B8=20=D1=83=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=89=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=D0=BB=D0=BE=D0=BA=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?-=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=BF=D0=BE=D0=B4=D1=85=D0=BE=D0=B4=20=D1=81=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=B0=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=B0=20=D0=BE?= =?UTF-8?q?=D1=82=D0=B2=D0=B5=D1=87=D0=B0=D1=8E=D1=89=D0=B8=D0=B5=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=B7=D0=B0=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E=20=D0=B8=20=D0=BF=D0=B0=D0=B3=D0=B8?= =?UTF-8?q?=D0=BD=D0=B0=D1=86=D0=B8=D1=8E=20=20=20=20=20=20=20=20=20-=20?= =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=20=D0=BF=D0=B0=D1=80=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D1=80=20len=5Fwithout=5Fconfines,=20=D1=83?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B7=D0=BB=D0=BD=D0=B5=D0=BD=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D1=85=D0=BE=D0=B4=20=D1=81=20=D1=85=D0=B0=D1=80=D0=B4?= =?UTF-8?q?=D0=BA=D0=BE=D0=B4=D0=BE=D0=BC=20=D0=BE=D0=B6=D0=B8=D0=B4=D0=B0?= =?UTF-8?q?=D0=B5=D0=BC=D0=BE=D0=B3=D0=BE=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB?= =?UTF-8?q?=D1=8C=D1=82=D0=B0=D1=82=D0=B0=20=20=20=20=20-=20=D0=9F=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=82=205=20(=D0=B3=D0=B0=D0=BB=D0=BB=D1=8E=D1=86?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0=D1=86=D0=B8=D1=8F)=20=D1=83=20=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D0=BE=D0=B4=D0=B0=20model=5Fvalidate=20=D1=82=D0=B0?= =?UTF-8?q?=D0=BA=20=D0=B6=D0=B5=20=D0=B5=D1=81=D1=82=D1=8C=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=20extra(=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B8=D0=BB,=20=D1=87=D1=82=D0=BE=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5=D1=82)=20=D1=81=D1=81?= =?UTF-8?q?=D1=8B=D0=BB=D0=BA=D0=B0=20=D0=BD=D0=B0=20=D0=B4=D0=BE=D0=BA?= =?UTF-8?q?=D1=83:=20=20=20=20=20https://pydantic.dev/docs/validation/late?= =?UTF-8?q?st/api/pydantic/base=5Fmodel/#pydantic.BaseModel.model=5Fvalida?= =?UTF-8?q?te=20=20=20=20=20-=20=D0=9F=D1=83=D0=BD=D0=BA=D1=82=2011=20?= =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BC=D1=83=D1=82=D0=B0=D1=86=D0=B8=D1=8F=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20body=20=20=20=20=20-=20?= =?UTF-8?q?=D0=9F=D1=83=D0=BD=D0=BA=D1=82=2012=20=20=20=20=20=20=20=20=20-?= =?UTF-8?q?=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=89=D0=B0=D1=8F=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D0=B0=D1=8F=20=D1=82=D0=BE=D1=87=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B2=20conftest.py=20=20=20=20=20=20=20=20=20-=20=D0=98=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B2=D1=80?= =?UTF-8?q?=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=B2=D0=B0=D0=B5=D0=BC=D1=8B=D1=85=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0=D0=BB=D0=BE=D0=BA=20(=D0=A1=D0=BE=D1=80=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BD=D1=8B=D1=85=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BC=D0=B5=D1=82=D0=BE=D0=BA=20?= =?UTF-8?q?=D0=BD=D0=B5=20=20=20=20=20=20=20=20=20=D0=B4=D0=B5=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B8=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B2=20=D1=81=D0=B0=D0=BC=D0=BE=D0=B9=20=D0=B1=D0=B8?= =?UTF-8?q?=D0=B7=D0=BD=D0=B5=D1=81-=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B5,?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=8D=D1=82=D0=BE=D0=BC=D1=83=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=B8=D1=82=D1=8C=20=D1=81=D0=BE=D1=80?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D1=83=20=D0=B2=20=D1=82?= =?UTF-8?q?=D0=B0=D0=BA=D0=BE=D0=BC=20=D1=81=D0=BB=D1=83=D1=87=D0=B0=D0=B5?= =?UTF-8?q?=20=D0=BD=D0=B5=20=D0=B1=D1=83=D0=B4=D0=B5=D1=82=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE.=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=D0=A1=D0=B5=D0=B9=D1=87=D0=B0=D1=81=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BA=D0=B0=D0=B6=D0=B4=D0=BE=D0=B9=20=D0=BC=D0=BE=D0=B4=D0=B0?= =?UTF-8?q?=D0=BB=D0=BA=D0=B8=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B0?= =?UTF-8?q?=20=D1=83=D0=BD=D0=B8=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F?= =?UTF-8?q?=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D0=BA=D0=B0)=20=20=20=20=20=20=20=20=20-=20?= =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D1=81=D1=82=D1=83=D1=80=D0=B0=20mock=5Fdatetime=5F?= =?UTF-8?q?now=20=D0=B4=D0=BB=D1=8F=20=D1=80=D1=83=D1=87=D0=BA=D0=B8=20upd?= =?UTF-8?q?ate=5Fstatus(=D0=B2=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D0=B5=20=D0=B2=D1=8B=D0=B7=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=D1=81?= =?UTF-8?q?=D1=8F=20datetime.now),=20=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B0=D1=85=20=D0=B7=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=81=D0=B8=D0=BC=D1=8B=D1=85=20=D0=BE=D1=82=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B5=D0=BD=D0=B8=20=D0=BE=D0=BD=D0=BE=20=D0=B1?= =?UTF-8?q?=D1=8B=D0=BB=D0=BE=20=D0=B5=D0=B4=D0=B8=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D1=82=D0=BE=D1=87=D0=BA=D0=BE=D0=B9=20=D0=BE=D1=82=D1=81=D1=87?= =?UTF-8?q?=D0=B5=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 51 ++- tests/test_routes/test_groups.py | 64 ++- tests/test_routes/test_note_type.py | 74 ++++ tests/test_routes/test_notes.py | 626 +++++++++++++--------------- tests/test_routes/test_services.py | 62 ++- 5 files changed, 471 insertions(+), 406 deletions(-) create mode 100644 tests/test_routes/test_note_type.py diff --git a/tests/conftest.py b/tests/conftest.py index 0e138cf..3a2ea90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,8 @@ from modal_backend.models.db import Group, Service, ModalStatus, Note from modal_backend.settings import Settings +NOW = datetime.now(timezone.utc).replace(tzinfo=None) + class PostgresConfig: """Дата-класс со значениями для контейнера с тестовой БД и для alembic-миграции.""" @@ -153,7 +155,7 @@ def create_note_type(name: str, type_id: int): @pytest.fixture() def note_types(dbsession): - """Создает три разных типа модалок.""" + """Создает пять разных типа модалок.""" note_type_data = [ ("info", 1), ("rating", 2), @@ -211,6 +213,12 @@ def services(dbsession): dbsession.commit() +@pytest.fixture() +def mock_datetime_now(mocker): + mock_datetime = mocker.patch("modal_backend.utils.services.datetime") + mock_datetime.now.return_value = NOW + yield + def create_note( type_id: int, schema: NoteChoicePost | NoteImagePost | NoteInfoPost | NoteRatingPost | NoteTextPost, @@ -234,7 +242,16 @@ def notes( services, authlib_user_data, ): - """Создает 7 модалок: 5 с разными типами, две просроченные.""" + """Создает 8 модалок: + indexes: descriprion: + (0, 1, 2, 3, 4) 5 с разными типами + (0, 1, 2) 3 активные + (3, 4, 5, 6, 7) 5 архивных + (0, 2, 4, 6, 7) group_ids = [3] service_ids = [3] + (1, 3) group_ids = [1, 2] service_ids = [1, 2] + (5) group_ids - [1, 2, 3] service_ids = [1, 2, 3] + (6, 7) просроченные, одна с is_always=True + """ note_data = [ { "type_id": note_types[0].type_id, @@ -296,13 +313,27 @@ def notes( "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.id for group in groups], + service_ids=[service.id for service in services], + ), + "admin_id": authlib_user_data.get("id"), + "status": ModalStatus.ARCHIVED, + }, + ] + for offset, d in enumerate(note_data): - d["schema"].start_ts = datetime.now(timezone.utc).replace(tzinfo=None) + offset * timedelta(hours=1) - d["schema"].end_ts = datetime.now(timezone.utc).replace(tzinfo=None) + offset * timedelta(hours=1) + d["schema"].start_ts = NOW + offset * timedelta(hours=1) + d["schema"].end_ts = NOW + offset * timedelta(hours=1) + timedelta(hours=1) note_data.extend( [ - { # просроченная модалка index 5 + { # просроченная модалка index 6 "type_id": note_types[4].type_id, "schema": NoteImagePost( header="header_6", @@ -310,13 +341,13 @@ def notes( frequency=10, group_ids=[group.id for group in groups][2:3], service_ids=[service.id for service in services][2:3], - start_ts=datetime.now(), - end_ts=datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1), + start_ts=NOW + timedelta(hours=2) * 5, + end_ts= NOW - timedelta(hours=1) * 5, ), "admin_id": authlib_user_data.get("id"), "status": ModalStatus.ARCHIVED, }, - { # просроченная модалка c is_always=True index 6 + { # просроченная модалка c is_always=True index 7 "type_id": note_types[4].type_id, "schema": NoteImagePost( header="header_7", @@ -324,8 +355,8 @@ def notes( frequency=10, group_ids=[group.id for group in groups][2:3], service_ids=[service.id for service in services][2:3], - start_ts=datetime.now(), - end_ts=datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1), + start_ts=NOW + timedelta(hours=3) * 5, + end_ts=NOW - timedelta(hours=1) * 5, ), "admin_id": authlib_user_data.get("id"), "status": ModalStatus.ARCHIVED, diff --git a/tests/test_routes/test_groups.py b/tests/test_routes/test_groups.py index f41207d..f1cdc36 100644 --- a/tests/test_routes/test_groups.py +++ b/tests/test_routes/test_groups.py @@ -9,17 +9,6 @@ settings = get_settings() -@pytest.mark.parametrize( - "status_code", - [ - (status.HTTP_200_OK), - ], -) -def test_get_groups(client, groups, status_code): - response = client.get(url) - assert response.status_code == status_code - - @pytest.mark.parametrize( "status_code, body", [ @@ -31,46 +20,30 @@ def test_get_groups(client, groups, status_code): ) def test_post_group(client, dbsession, groups, status_code, body): response = client.post(url, json=body) - assert response.status_code == response.status_code + 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).one_or_none() + exist_group = Group.query(session=dbsession).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) + dbsession.commit() @pytest.mark.parametrize( - "status_code, group_n", + "status_code", [ - ( - status.HTTP_200_OK, - 1, - ), - ( - status.HTTP_404_NOT_FOUND, - 999, - ), - ( - status.HTTP_422_UNPROCESSABLE_CONTENT, - "abc", - ), + (status.HTTP_200_OK), ], ) -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}") +def test_get_groups(client, status_code): + response = client.get(url) 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( @@ -121,7 +94,28 @@ def test_update_group(client, dbsession, groups, status_code, body, group_n): 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() + exist_group = ( + Group.query(session=dbsession).filter(Group.id == response_model.id).populate_existing().one_or_none() + ) assert exist_group assert exist_group.group_id == body.get("group_id") assert exist_group.name == body.get("name") + + +@pytest.mark.parametrize( + "status_code, group_n", + [ + (status.HTTP_200_OK, 1,), + (status.HTTP_404_NOT_FOUND, 99,), + (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 diff --git a/tests/test_routes/test_note_type.py b/tests/test_routes/test_note_type.py new file mode 100644 index 0000000..227768d --- /dev/null +++ b/tests/test_routes/test_note_type.py @@ -0,0 +1,74 @@ +import pytest +from starlette import status + +from modal_backend.models import NoteType +from modal_backend.schemas.models import NoteTypeGet +from modal_backend.settings import get_settings + +url: str = "/notificationtype" +settings = get_settings() + + +@pytest.mark.parametrize( + "status_code, body", + [ + ( + status.HTTP_200_OK, + { + "type_id": 6, + "name": "No_exist_type", + }, + ), + ( + status.HTTP_409_CONFLICT, + { + "type_id": 1, + "name": "Already_exist_type", + }, + ), + ( + status.HTTP_422_UNPROCESSABLE_ENTITY, + { + "type_id": 6, + "name": 123, + }, + ), + ( + status.HTTP_422_UNPROCESSABLE_ENTITY, + { + "type_id": "string", + "name": "string", + }, + ), + ], +) +def test_post_create_notification_type(client, dbsession, note_types, status_code, body): + response = client.post(url, json=body) + assert response.status_code == status_code + + if status_code == status.HTTP_200_OK: + response_model = NoteTypeGet.model_validate(response.json(), extra="forbid") + exist_note_type = NoteType.query(session=dbsession).filter(NoteType.type_id == response_model.type_id).one_or_none() + assert exist_note_type + 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) + dbsession.commit() + + +@pytest.mark.parametrize( + "status_code", + [ + (status.HTTP_200_OK), + ], +) +def test_get_notification_type(client, note_types, status_code): + response = client.get(url) + assert response.status_code == status_code + type_ids_of_note_types = [note_type.type_id for note_type in note_types] + for note_type in response.json(): + assert note_type.get("type_id") in type_ids_of_note_types + + diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index c116ab0..e963d96 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -1,5 +1,6 @@ import pytest from starlette import status +from typing import Any from modal_backend.models.db import Group, ModalStatus, Note, Service from modal_backend.schemas.models import NoteChoiceGet, NoteImageGet, NoteInfoGet, NoteRatingGet, NoteTextGet @@ -9,8 +10,242 @@ settings = get_settings() +def resolve_items(n_list: list | Any, items: list) -> list | Any: + """ + Вспомогательная функция для перевода индексов списков групп и сервисов + с учётом возможности указывать невалидные значения(для негативных кейсов) + """ + indexes = range(len(items)) + list_ids = [] + if isinstance(n_list, list): + for n in n_list: + if n in indexes: + list_ids.append(items[n].id) + else: + list_ids.append(n) + else: + list_ids = n_list + return list_ids + + +@pytest.mark.parametrize( + "status_code, body, type_model, path", + [ + ( + status.HTTP_200_OK, + { + "header": "string", + "group_ids": [0], # индексы + "service_ids": [0], # индексы + "frequency": 0, + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": False, + "info_text": "string", + }, + NoteInfoGet, + "/info", + ), + ( + status.HTTP_200_OK, + { + "header": "string", + "group_ids": [0], # индексы + "service_ids": [0], # индексы + "frequency": 0, + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": True, + "rating_max": 0, + }, + NoteRatingGet, + "/rating", + ), + ( + status.HTTP_200_OK, + { + "header": "string", + "group_ids": [0], # индексы + "service_ids": [0], # индексы + "frequency": 0, + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": False, + "text": "string", + "max_length": 6, + }, + NoteTextGet, + "/text", + ), + ( + status.HTTP_200_OK, + { + "header": "string", + "group_ids": [0], # индексы + "service_ids": [0], # индексы + "frequency": 0, + "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"}], + "is_multiple": True, + }, + NoteChoiceGet, + "/choice", + ), + ( + status.HTTP_200_OK, + { + "header": "string", + "group_ids": [0], # индексы + "service_ids": [0], # индексы + "frequency": 0, + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": False, + "images": ["string"], + }, + NoteImageGet, + "/image", + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + { + "header": "string", + "group_ids": "two", # индексы + "service_ids": [1, "two", 3], # индексы + "frequency": "digit", + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": None, + "info_text": "string", + }, + NoteInfoGet, + "/info", + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + { + "header": "string", + "group_ids": "two", # индексы + "service_ids": [1, "two", 3], # индексы + "frequency": "digit", + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": None, + "max_rating": "string", + }, + NoteRatingGet, + "/rating", + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + { + "header": "string", + "group_ids": "two", # индексы + "service_ids": [1, "two", 3], # индексы + "frequency": "digit", + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": None, + "text": "string", + "max_length": "string", + }, + NoteTextGet, + "/text", + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + { + "header": "string", + "group_ids": "two", # индексы + "service_ids": [1, "two", 3], # индексы + "frequency": "digit", + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": None, + "choice_options": [{"id": 0, "text": "string"}], + "is_multiple": None, + }, + NoteChoiceGet, + "/choice", + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + { + "header": "string", + "group_ids": "two", # индексы + "service_ids": [1, "two", 3], # индексы + "frequency": "digit", + "start_ts": "2026-08-04T17:21:45.694Z", + "end_ts": "2026-08-04T17:22:45.694Z", + "is_always": None, + "images": ["string"], + }, + NoteImageGet, + "/image", + ), + ], +) +def test_create_all_type_of_note(client, dbsession, groups, services, note_types, status_code, body, type_model, path): + + json_body = body + group_n_list = json_body.get("group_ids") + service_n_list = json_body.get("service_ids") + groups_id = resolve_items(group_n_list, groups) + services_id = resolve_items(service_n_list, services) + + json_body["group_ids"] = groups_id + json_body["service_ids"] = services_id + + response = client.post(f"{url}{path}", json=json_body) + assert response.status_code == status_code + + if status_code == status.HTTP_200_OK: + response_model = type_model.model_validate(response.json(), extra="forbid") + note = Note.query(session=dbsession).filter(Note.id == response_model.id).one_or_none() + assert note + assert response_model.type_id == note.type_id + assert response_model.header == note.header + assert response_model.status == note.status + assert response_model.admin_id == note.admin_id + + if type_model is NoteTextGet: + assert len(note.text) <= note.max_length + dbsession.delete(note) + + +def calculate_expected_len(all_notes, + type_id: int | None, + groups_id: list[int] | None, + services_id: list[int] | None, + status: str | None, + limit: int, + offset: int, + asc_order: bool | None + ) -> list[Note]: + """ + Вспомогательная функция для подсчета корректной длины списка модалок + документирующая контракт пагинации и фильтрации + в modal_backend/utils/service.py::NoteService.get_note_by_filters + """ + filtered_notes = all_notes + if type_id is not None: + filtered_notes = [note for note in filtered_notes if note.type_id == type_id] + if groups_id is not None: + filtered_notes = [note for note in filtered_notes if any(g in note.group_ids for g in groups_id)] + if services_id is not None: + filtered_notes = [note for note in filtered_notes if any(s in note.service_ids for s in services_id)] + if status is not None: + filtered_notes = [note for note in filtered_notes if note.status == status] + + filtered_notes = sorted(filtered_notes, key=lambda note: note.start_ts, reverse=(asc_order is not True)) + + return [note.id for note in filtered_notes[offset:limit + offset]] + + @pytest.mark.parametrize( - "status_code, type_id, group_n_list, service_n_list, modal_status, asc_order, limit, offset, len_without_confines", + "status_code, type_id, group_n_list, service_n_list, modal_status, asc_order, limit, offset", [ # позитивные кейсы(объединенные проверки) ( # все модалки @@ -22,7 +257,6 @@ None, # asc_order None, # limit None, # offset - 7, # len_without_confines ), ( # активные - ограничение по лимиту и смещению + порядок + группы status.HTTP_200_OK, @@ -33,7 +267,6 @@ True, # asc_order 2, # limit 1, # offset - 2, # len_without_confines ), ( # архив - ограничение по лимиту и смещению + порядок status.HTTP_200_OK, @@ -44,18 +277,36 @@ False, # asc_order 999, # limit 1, # offset - 4, # len_without_confines ), - ( # ограничение по группам и сервисам + ( status.HTTP_404_NOT_FOUND, None, # type_id + [0, 1, 2], # group_n_list + [0, 1, 2], # service_n_list + "archived", # modal_status + False, # asc_order + None, # limit + 5, # offset + ), + ( # ограничение по группам и сервисам + status.HTTP_200_OK, + None, # type_id [0, 1], # group_n_list [2], # service_n_list None, # modal_status None, # asc_order None, # limit None, # offset - 0, # len_without_confines + ), + ( # не существующие группы и сервисы + status.HTTP_404_NOT_FOUND, + None, # type_id + [4], # group_n_list + [4], # service_n_list + None, # modal_status + None, # asc_order + None, # limit + None, # offset ), ( # ограничение по типу модалки status.HTTP_200_OK, @@ -66,7 +317,6 @@ None, # asc_order None, # limit None, # offset - 1, # len_without_confines ), ( # нулевой лимит status.HTTP_404_NOT_FOUND, @@ -77,7 +327,6 @@ None, # asc_order 0, # limit None, # offset - 0, # len_without_confines ), # негативные кейсы ( # отрицательный лимит + не валидный лимит @@ -89,7 +338,6 @@ None, # asc_order -1, # limit None, # offset - 0, # len_without_confines ), ( # отрицательное смещение + не валидное смещение status.HTTP_422_UNPROCESSABLE_CONTENT, @@ -100,7 +348,6 @@ None, # asc_order None, # limit -1, # offset - 0, # len_without_confines ), ( # offset превышающее lwc и limit status.HTTP_404_NOT_FOUND, @@ -111,7 +358,6 @@ None, # asc_order 4, # limit 999, # offset - 0, # len_without_confines ), ( # не существующий type_id status.HTTP_404_NOT_FOUND, @@ -122,7 +368,6 @@ None, # asc_order None, # limit None, # offset - 0, # len_without_confines ), ( # не валидный type_id status.HTTP_422_UNPROCESSABLE_CONTENT, @@ -133,7 +378,6 @@ None, # asc_order None, # limit None, # offset - 0, # len_without_confines ), ( # не валидные group_n_list status.HTTP_422_UNPROCESSABLE_CONTENT, @@ -144,7 +388,6 @@ None, # asc_order None, # limit None, # offset - 0, # len_without_confines ), ( # не валидные service_ids status.HTTP_422_UNPROCESSABLE_CONTENT, @@ -155,7 +398,6 @@ None, # asc_order None, # limit None, # offset - 0, # len_without_confines ), ( # не валидный status status.HTTP_422_UNPROCESSABLE_CONTENT, @@ -166,7 +408,6 @@ None, # asc_order None, # limit None, # offset - 0, # len_without_confines ), ( # не валидные asc_order status.HTTP_422_UNPROCESSABLE_CONTENT, @@ -177,13 +418,11 @@ 999, # asc_order None, # limit None, # offset - 0, # len_without_confines ), ], ) def test_get_notes( client, - dbsession, notes, groups, services, @@ -195,30 +434,10 @@ def test_get_notes( asc_order, limit, offset, - len_without_confines, ): # Добавление id групп и сервисов с проверкой типа, чтобы можно было указать не валидные query-параметры. - group_indexes = range(len(groups)) - service_indexes = range(len(services)) - groups_id = [] - services_id = [] - if isinstance(group_n_list, list): - for group_n in group_n_list: - if group_n in group_indexes: - groups_id.append(groups[group_n].id) - else: - groups_id.append(group_n) - else: - groups_id = group_n_list - - if isinstance(service_n_list, list): - for service_n in service_n_list: - if service_n in service_indexes: - services_id.append(services[service_n].id) - else: - services_id.append(service_n) - else: - services_id = service_n_list + groups_id = resolve_items(group_n_list, groups) + services_id = resolve_items(service_n_list, services) dict_of_params = { "type_id": type_id if type_id is not None else None, @@ -236,40 +455,32 @@ def test_get_notes( 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}" + ids_in_response = [note.get("id") for note in response_data] # проверяем порядок - 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:])) - + start_ts_by_id = {note.id : note.start_ts for note in notes} + ts_data = [start_ts_by_id[note_id] for note_id in ids_in_response] + if query.get("asc_order"): + assert ts_data == sorted(ts_data) + else: + assert ts_data == sorted(ts_data, reverse=True) + + # проверка контракта фильтрации и пагинации(содержимого и длины после урезания limit-ом и offset-ом) + expected_note_ids = calculate_expected_len(notes, + type_id=query.get("type_id"), + groups_id=query.get("groups_id"), + services_id=query.get("services_id"), + status=query.get("status"), + limit=query.get("limit", 10), + offset=query.get("offset", 0), + asc_order=query.get("asc_order"), + ) + + assert ids_in_response == expected_note_ids # проверка корректности данных отфильтрованных модалок - 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 + for resp_obj in response_data: + if type_id: assert resp_obj.get("type_id") == type_id + if modal_status: assert resp_obj.get("status") == modal_status @pytest.mark.parametrize( @@ -326,53 +537,18 @@ def test_get_note_by_id(client, notes, status_code, note_n, type_model): @pytest.mark.parametrize( "status_code, note_n, modal_status, deleted_group_id_flag, deleted_service_id_flag", [ - ( - status.HTTP_200_OK, - 0, - ModalStatus.ARCHIVED, - False, - False, - ), - ( - status.HTTP_404_NOT_FOUND, - -1, - ModalStatus.ACTIVE, - False, - False, - ), - (status.HTTP_200_OK, 3, ModalStatus.ACTIVE, False, False), - ( - status.HTTP_403_FORBIDDEN, - 3, - ModalStatus.ACTIVE, - True, - False, - ), - ( - status.HTTP_403_FORBIDDEN, - 3, - ModalStatus.ACTIVE, - False, - True, - ), - ( # просроченная модалка - status.HTTP_403_FORBIDDEN, - 5, - ModalStatus.ACTIVE, - False, - False, - ), - ( # просроченная модалка с is_always=True - status.HTTP_200_OK, - 6, - ModalStatus.ACTIVE, - False, - False, - ), + (status.HTTP_200_OK, 0, ModalStatus.ARCHIVED, False, False, ), + (status.HTTP_404_NOT_FOUND, -1, ModalStatus.ACTIVE, False, False, ), + (status.HTTP_200_OK, 3, ModalStatus.ACTIVE, False, False, ), + (status.HTTP_403_FORBIDDEN, 3, ModalStatus.ACTIVE, True, False, ), + (status.HTTP_403_FORBIDDEN, 3, ModalStatus.ACTIVE, False, True, ), + (status.HTTP_403_FORBIDDEN, 3, ModalStatus.ACTIVE, True, True, ), + (status.HTTP_403_FORBIDDEN, 6, ModalStatus.ACTIVE, False, False, ), # просроченная модалка + (status.HTTP_200_OK, 7, ModalStatus.ACTIVE, False, False, ), # просроченная модалка с is_always=True ], ) def test_update_note_status( - client, dbsession, notes, note_n, status_code, modal_status, deleted_group_id_flag, deleted_service_id_flag + client, dbsession, mock_datetime_now, notes, note_n, status_code, modal_status, deleted_group_id_flag, deleted_service_id_flag ): notes_indexes = range(len(notes)) @@ -380,13 +556,13 @@ def test_update_note_status( if deleted_group_id_flag: for group_id in notes[note_n].group_ids: - group = Group.query(session=dbsession).filter(Group.id == group_id).first() - dbsession.delete(group) + group = Group.query(session=dbsession).filter(Group.id == group_id).one() + group.is_deleted = True dbsession.commit() if deleted_service_id_flag: for service_id in notes[note_n].service_ids: - service = Service.query(session=dbsession).filter(Service.id == service_id).first() - dbsession.delete(service) + service = Service.query(session=dbsession).filter(Service.id == service_id).one() + service.is_deleted = True dbsession.commit() response = client.patch(f"{url}/{id_of_note}/status") @@ -394,7 +570,7 @@ def test_update_note_status( if status_code == status.HTTP_200_OK: response_data = response.json() - note = Note.query(session=dbsession).filter(Note.id == response_data.get("id")).populate_existing().first() + note = Note.query(session=dbsession).filter(Note.id == response_data.get("id")).populate_existing().one() assert note.status == modal_status @@ -413,209 +589,7 @@ def test_delete_note(client, dbsession, notes, status_code, note_n): response = client.delete(f"{url}/{id_of_note}") assert response.status_code == status_code if status_code == status.HTTP_200_OK: - deleted_note = Note.query(session=dbsession).filter(Note.id == id_of_note).populate_existing().one_or_none - assert deleted_note is not None - - -@pytest.mark.parametrize( - "status_code, body, type_model, path", - [ - ( - status.HTTP_200_OK, - { - "header": "string", - "group_ids": [0], # индексы - "service_ids": [0], # индексы - "frequency": 0, - "start_ts": "2026-08-04T17:21:45.694Z", - "end_ts": "2026-08-04T17:22:45.694Z", - "is_always": False, - "info_text": "string", - }, - NoteInfoGet, - "/info", - ), - ( - status.HTTP_200_OK, - { - "header": "string", - "group_ids": [0], # индексы - "service_ids": [0], # индексы - "frequency": 0, - "start_ts": "2026-08-04T17:21:45.694Z", - "end_ts": "2026-08-04T17:22:45.694Z", - "is_always": True, - "rating_max": 0, - }, - NoteRatingGet, - "/rating", - ), - ( - status.HTTP_200_OK, - { - "header": "string", - "group_ids": [0], # индексы - "service_ids": [0], # индексы - "frequency": 0, - "start_ts": "2026-08-04T17:21:45.694Z", - "end_ts": "2026-08-04T17:22:45.694Z", - "is_always": False, - "text": "string", - "max_length": 6, - }, - NoteTextGet, - "/text", - ), - ( - status.HTTP_200_OK, - { - "header": "string", - "group_ids": [0], # индексы - "service_ids": [0], # индексы - "frequency": 0, - "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"}], - "is_multiple": True, - }, - NoteChoiceGet, - "/choice", - ), - ( - status.HTTP_200_OK, - { - "header": "string", - "group_ids": [0], # индексы - "service_ids": [0], # индексы - "frequency": 0, - "start_ts": "2026-08-04T17:21:45.694Z", - "end_ts": "2026-08-04T17:22:45.694Z", - "is_always": False, - "images": ["string"], - }, - NoteImageGet, - "/image", - ), - ( - status.HTTP_422_UNPROCESSABLE_CONTENT, - { - "header": "string", - "group_ids": "two", # индексы - "service_ids": [1, "two", 3], # индексы - "frequency": "digit", - "start_ts": "2026-08-04T17:21:45.694Z", - "end_ts": "2026-08-04T17:22:45.694Z", - "is_always": None, - "info_text": "string", - }, - NoteInfoGet, - "/info", - ), - ( - status.HTTP_422_UNPROCESSABLE_CONTENT, - { - "header": "string", - "group_ids": "two", # индексы - "service_ids": [1, "two", 3], # индексы - "frequency": "digit", - "start_ts": "2026-08-04T17:21:45.694Z", - "end_ts": "2026-08-04T17:22:45.694Z", - "is_always": None, - "max_rating": "string", - }, - NoteRatingGet, - "/rating", - ), - ( - status.HTTP_422_UNPROCESSABLE_CONTENT, - { - "header": "string", - "group_ids": "two", # индексы - "service_ids": [1, "two", 3], # индексы - "frequency": "digit", - "start_ts": "2026-08-04T17:21:45.694Z", - "end_ts": "2026-08-04T17:22:45.694Z", - "is_always": None, - "text": "string", - "max_length": "string", - }, - NoteTextGet, - "/text", - ), - ( - status.HTTP_422_UNPROCESSABLE_CONTENT, - { - "header": "string", - "group_ids": "two", # индексы - "service_ids": [1, "two", 3], # индексы - "frequency": "digit", - "start_ts": "2026-08-04T17:21:45.694Z", - "end_ts": "2026-08-04T17:22:45.694Z", - "is_always": None, - "choice_options": [{"id": 0, "text": "string"}], - "is_multiple": None, - }, - NoteChoiceGet, - "/choice", - ), - ( - status.HTTP_422_UNPROCESSABLE_CONTENT, - { - "header": "string", - "group_ids": "two", # индексы - "service_ids": [1, "two", 3], # индексы - "frequency": "digit", - "start_ts": "2026-08-04T17:21:45.694Z", - "end_ts": "2026-08-04T17:22:45.694Z", - "is_always": None, - "images": ["string"], - }, - NoteImageGet, - "/image", - ), - ], -) -def test_create_all_type_of_note(client, dbsession, groups, services, note_types, status_code, body, type_model, path): - group_indexes = range(len(groups)) - service_indexes = range(len(services)) - group_n_list = body.get("group_ids") - service_n_list = body.get("service_ids") - groups_id = [] - services_id = [] - if isinstance(group_n_list, list): - for group_n in group_n_list: - if group_n in group_indexes: - groups_id.append(groups[group_n].id) - else: - groups_id.append(group_n) - else: - groups_id = group_n_list - - if isinstance(service_n_list, list): - for service_n in service_n_list: - if service_n in service_indexes: - services_id.append(services[service_n].id) - else: - services_id.append(service_n) - else: - services_id = service_n_list - - body["group_ids"] = groups_id - body["service_ids"] = services_id - - response = client.post(f"{url}{path}", json=body) - assert response.status_code == status_code - - if status_code == status.HTTP_200_OK: - response_model = type_model.model_validate(response.json(), extra="forbid") - note = Note.query(session=dbsession).filter(Note.id == response_model.id).one_or_none() - assert note - assert response_model.type_id == note.type_id - assert response_model.header == note.header - assert response_model.status == note.status - assert response_model.admin_id == note.admin_id - - if type_model is NoteTextGet: - assert len(note.text) <= note.max_length - dbsession.delete(note) + deleted_note = ( + dbsession.query(Note).filter(Note.id == id_of_note).populate_existing().one_or_none() + ) + assert deleted_note.is_deleted diff --git a/tests/test_routes/test_services.py b/tests/test_routes/test_services.py index c6031ac..b3fdc33 100644 --- a/tests/test_routes/test_services.py +++ b/tests/test_routes/test_services.py @@ -9,17 +9,6 @@ settings = get_settings() -@pytest.mark.parametrize( - "status_code", - [ - (status.HTTP_200_OK), - ], -) -def test_get_services(client, services, status_code): - response = client.get(url) - assert response.status_code == status_code - - @pytest.mark.parametrize( "status_code, body", [ @@ -31,46 +20,30 @@ def test_get_services(client, services, status_code): ) def test_post_service(client, dbsession, services, status_code, body): response = client.post(url, json=body) - assert response.status_code == response.status_code + 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).one_or_none() + exist_service = Service.query(session=dbsession).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) + dbsession.commit() @pytest.mark.parametrize( - "status_code, service_n", + "status_code", [ - ( - status.HTTP_200_OK, - 1, - ), - ( - status.HTTP_404_NOT_FOUND, - 999, - ), - ( - status.HTTP_422_UNPROCESSABLE_CONTENT, - "abc", - ), + (status.HTTP_200_OK), ], ) -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}") +def test_get_services(client, status_code): + response = client.get(url) 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( @@ -121,8 +94,27 @@ def test_update_service(client, dbsession, services, status_code, body, service_ if status_code == status.HTTP_200_OK: response_model = ServiceGet.model_validate(response.json(), extra="forbid") exist_service = ( - dbsession.query(Service).filter(Service.id == response_model.id).populate_existing().one_or_none() + Service.query(session=dbsession).filter(Service.id == response_model.id).populate_existing().one_or_none() ) assert exist_service assert exist_service.service_id == body.get("service_id") assert exist_service.name == body.get("name") + + +@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 From 721d11a17801d17b36cd6057a9de9f7e6300fb02 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 15 Aug 2026 17:02:12 +0300 Subject: [PATCH 12/14] =?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?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 55 +++-------- tests/test_routes/test_groups.py | 19 +++- tests/test_routes/test_note_type.py | 74 -------------- tests/test_routes/test_notes.py | 143 ++++++++++++++++++++-------- tests/test_routes/test_services.py | 15 ++- 5 files changed, 139 insertions(+), 167 deletions(-) delete mode 100644 tests/test_routes/test_note_type.py diff --git a/tests/conftest.py b/tests/conftest.py index 3a2ea90..7b861d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer +from modal_backend.models.db import Group, ModalStatus, NoteTypeEnum, Note, Service from modal_backend.schemas.models import ( NoteChoicePost, NoteImagePost, @@ -18,7 +19,6 @@ NoteRatingPost, NoteTextPost, ) -from modal_backend.models.db import Group, Service, ModalStatus, Note from modal_backend.settings import Settings NOW = datetime.now(timezone.utc).replace(tzinfo=None) @@ -144,35 +144,6 @@ def client(get_app_with_test_settings, user_mock): app = get_app_with_test_settings client = TestClient(app) return client -<<<<<<< HEAD -======= - - -def create_note_type(name: str, type_id: int): - """Вспомогательная функция-мини-фабрика для создания разных типов модалок в фикстуре note_types.""" - return NoteType(name=name, type_id=type_id) - - -@pytest.fixture() -def note_types(dbsession): - """Создает пять разных типа модалок.""" - note_type_data = [ - ("info", 1), - ("rating", 2), - ("text", 3), - ("choice", 4), - ("image", 5), - ] - - note_types = [create_note_type(*note_type) for note_type in note_type_data] - - for note_type in note_types: - dbsession.add(note_type) - dbsession.commit() - yield note_types - for note_type in note_types: - dbsession.delete(note_type) - dbsession.commit() def create_group(group_id: int, name: str) -> Group: @@ -219,6 +190,7 @@ def mock_datetime_now(mocker): mock_datetime.now.return_value = NOW yield + def create_note( type_id: int, schema: NoteChoicePost | NoteImagePost | NoteInfoPost | NoteRatingPost | NoteTextPost, @@ -233,16 +205,14 @@ def create_note( ) - @pytest.fixture() def notes( dbsession, - note_types, groups, services, authlib_user_data, ): - """Создает 8 модалок: + """Создает 8 модалок: indexes: descriprion: (0, 1, 2, 3, 4) 5 с разными типами (0, 1, 2) 3 активные @@ -254,7 +224,7 @@ def notes( """ note_data = [ { - "type_id": note_types[0].type_id, + "type_id": NoteTypeEnum.INFO, "schema": NoteInfoPost( header="header_1", is_always=False, @@ -266,7 +236,7 @@ def notes( "status": ModalStatus.ACTIVE, }, { - "type_id": note_types[1].type_id, + "type_id": NoteTypeEnum.RATING, "schema": NoteRatingPost( header="header_2", is_always=False, @@ -278,7 +248,7 @@ def notes( "status": ModalStatus.ACTIVE, }, { - "type_id": note_types[2].type_id, + "type_id": NoteTypeEnum.TEXT, "schema": NoteTextPost( header="header_3", is_always=False, @@ -290,7 +260,7 @@ def notes( "status": ModalStatus.ACTIVE, }, { - "type_id": note_types[3].type_id, + "type_id": NoteTypeEnum.CHOICE, "schema": NoteChoicePost( header="header_4", is_always=False, @@ -302,7 +272,7 @@ def notes( "status": ModalStatus.ARCHIVED, }, { - "type_id": note_types[4].type_id, + "type_id": NoteTypeEnum.IMAGE, "schema": NoteImagePost( header="header_5", is_always=False, @@ -314,7 +284,7 @@ def notes( "status": ModalStatus.ARCHIVED, }, { - "type_id": note_types[4].type_id, + "type_id": NoteTypeEnum.IMAGE, "schema": NoteImagePost( header="header_5", is_always=False, @@ -325,7 +295,6 @@ def notes( "admin_id": authlib_user_data.get("id"), "status": ModalStatus.ARCHIVED, }, - ] for offset, d in enumerate(note_data): @@ -334,7 +303,7 @@ def notes( note_data.extend( [ { # просроченная модалка index 6 - "type_id": note_types[4].type_id, + "type_id": NoteTypeEnum.IMAGE, "schema": NoteImagePost( header="header_6", is_always=False, @@ -342,13 +311,13 @@ def notes( group_ids=[group.id for group in groups][2:3], service_ids=[service.id for service in services][2:3], start_ts=NOW + timedelta(hours=2) * 5, - end_ts= NOW - timedelta(hours=1) * 5, + end_ts=NOW - timedelta(hours=1) * 5, ), "admin_id": authlib_user_data.get("id"), "status": ModalStatus.ARCHIVED, }, { # просроченная модалка c is_always=True index 7 - "type_id": note_types[4].type_id, + "type_id": NoteTypeEnum.IMAGE, "schema": NoteImagePost( header="header_7", is_always=True, diff --git a/tests/test_routes/test_groups.py b/tests/test_routes/test_groups.py index f1cdc36..9260cde 100644 --- a/tests/test_routes/test_groups.py +++ b/tests/test_routes/test_groups.py @@ -95,8 +95,8 @@ def test_update_group(client, dbsession, groups, status_code, body, group_n): response_data = response.json() response_model = GroupGet(**response_data) exist_group = ( - Group.query(session=dbsession).filter(Group.id == response_model.id).populate_existing().one_or_none() - ) + Group.query(session=dbsession).filter(Group.id == response_model.id).populate_existing().one_or_none() + ) assert exist_group assert exist_group.group_id == body.get("group_id") assert exist_group.name == body.get("name") @@ -105,9 +105,18 @@ def test_update_group(client, dbsession, groups, status_code, body, group_n): @pytest.mark.parametrize( "status_code, group_n", [ - (status.HTTP_200_OK, 1,), - (status.HTTP_404_NOT_FOUND, 99,), - (status.HTTP_422_UNPROCESSABLE_CONTENT, "abc",), + ( + status.HTTP_200_OK, + 1, + ), + ( + status.HTTP_404_NOT_FOUND, + 99, + ), + ( + status.HTTP_422_UNPROCESSABLE_CONTENT, + "abc", + ), ], ) def test_delete_group(client, dbsession, groups, status_code, group_n): diff --git a/tests/test_routes/test_note_type.py b/tests/test_routes/test_note_type.py deleted file mode 100644 index 227768d..0000000 --- a/tests/test_routes/test_note_type.py +++ /dev/null @@ -1,74 +0,0 @@ -import pytest -from starlette import status - -from modal_backend.models import NoteType -from modal_backend.schemas.models import NoteTypeGet -from modal_backend.settings import get_settings - -url: str = "/notificationtype" -settings = get_settings() - - -@pytest.mark.parametrize( - "status_code, body", - [ - ( - status.HTTP_200_OK, - { - "type_id": 6, - "name": "No_exist_type", - }, - ), - ( - status.HTTP_409_CONFLICT, - { - "type_id": 1, - "name": "Already_exist_type", - }, - ), - ( - status.HTTP_422_UNPROCESSABLE_ENTITY, - { - "type_id": 6, - "name": 123, - }, - ), - ( - status.HTTP_422_UNPROCESSABLE_ENTITY, - { - "type_id": "string", - "name": "string", - }, - ), - ], -) -def test_post_create_notification_type(client, dbsession, note_types, status_code, body): - response = client.post(url, json=body) - assert response.status_code == status_code - - if status_code == status.HTTP_200_OK: - response_model = NoteTypeGet.model_validate(response.json(), extra="forbid") - exist_note_type = NoteType.query(session=dbsession).filter(NoteType.type_id == response_model.type_id).one_or_none() - assert exist_note_type - 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) - dbsession.commit() - - -@pytest.mark.parametrize( - "status_code", - [ - (status.HTTP_200_OK), - ], -) -def test_get_notification_type(client, note_types, status_code): - response = client.get(url) - assert response.status_code == status_code - type_ids_of_note_types = [note_type.type_id for note_type in note_types] - for note_type in response.json(): - assert note_type.get("type_id") in type_ids_of_note_types - - diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index e963d96..4afe59d 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -1,8 +1,9 @@ +from typing import Any + import pytest from starlette import status -from typing import Any -from modal_backend.models.db import Group, ModalStatus, Note, Service +from modal_backend.models.db import Group, ModalStatus, NoteTypeEnum, Note, Service from modal_backend.schemas.models import NoteChoiceGet, NoteImageGet, NoteInfoGet, NoteRatingGet, NoteTextGet from modal_backend.settings import get_settings @@ -12,7 +13,7 @@ def resolve_items(n_list: list | Any, items: list) -> list | Any: """ - Вспомогательная функция для перевода индексов списков групп и сервисов + Вспомогательная функция для перевода индексов списков групп и сервисов с учётом возможности указывать невалидные значения(для негативных кейсов) """ indexes = range(len(items)) @@ -187,7 +188,7 @@ def resolve_items(n_list: list | Any, items: list) -> list | Any: ), ], ) -def test_create_all_type_of_note(client, dbsession, groups, services, note_types, status_code, body, type_model, path): +def test_create_all_type_of_note(client, dbsession, groups, services, status_code, body, type_model, path): json_body = body group_n_list = json_body.get("group_ids") @@ -215,15 +216,16 @@ def test_create_all_type_of_note(client, dbsession, groups, services, note_types dbsession.delete(note) -def calculate_expected_len(all_notes, - type_id: int | None, - groups_id: list[int] | None, - services_id: list[int] | None, - status: str | None, - limit: int, - offset: int, - asc_order: bool | None - ) -> list[Note]: +def calculate_expected_len( + all_notes, + type_id: int | None, + groups_id: list[int] | None, + services_id: list[int] | None, + status: str | None, + limit: int, + offset: int, + asc_order: bool | None, +) -> list[Note]: """ Вспомогательная функция для подсчета корректной длины списка модалок документирующая контракт пагинации и фильтрации @@ -233,15 +235,15 @@ def calculate_expected_len(all_notes, if type_id is not None: filtered_notes = [note for note in filtered_notes if note.type_id == type_id] if groups_id is not None: - filtered_notes = [note for note in filtered_notes if any(g in note.group_ids for g in groups_id)] + filtered_notes = [note for note in filtered_notes if any(g in note.group_ids for g in groups_id)] if services_id is not None: - filtered_notes = [note for note in filtered_notes if any(s in note.service_ids for s in services_id)] + filtered_notes = [note for note in filtered_notes if any(s in note.service_ids for s in services_id)] if status is not None: filtered_notes = [note for note in filtered_notes if note.status == status] filtered_notes = sorted(filtered_notes, key=lambda note: note.start_ts, reverse=(asc_order is not True)) - return [note.id for note in filtered_notes[offset:limit + offset]] + return [note.id for note in filtered_notes[offset : limit + offset]] @pytest.mark.parametrize( @@ -278,7 +280,7 @@ def calculate_expected_len(all_notes, 999, # limit 1, # offset ), - ( + ( status.HTTP_404_NOT_FOUND, None, # type_id [0, 1, 2], # group_n_list @@ -310,7 +312,7 @@ def calculate_expected_len(all_notes, ), ( # ограничение по типу модалки status.HTTP_200_OK, - 4, # type_id + NoteTypeEnum.CHOICE, # type_id None, # group_n_list None, # service_n_list None, # modal_status @@ -458,7 +460,7 @@ def test_get_notes( ids_in_response = [note.get("id") for note in response_data] # проверяем порядок - start_ts_by_id = {note.id : note.start_ts for note in notes} + start_ts_by_id = {note.id: note.start_ts for note in notes} ts_data = [start_ts_by_id[note_id] for note_id in ids_in_response] if query.get("asc_order"): assert ts_data == sorted(ts_data) @@ -466,21 +468,24 @@ def test_get_notes( assert ts_data == sorted(ts_data, reverse=True) # проверка контракта фильтрации и пагинации(содержимого и длины после урезания limit-ом и offset-ом) - expected_note_ids = calculate_expected_len(notes, - type_id=query.get("type_id"), - groups_id=query.get("groups_id"), - services_id=query.get("services_id"), - status=query.get("status"), - limit=query.get("limit", 10), - offset=query.get("offset", 0), - asc_order=query.get("asc_order"), - ) + expected_note_ids = calculate_expected_len( + notes, + type_id=query.get("type_id"), + groups_id=query.get("groups_id"), + services_id=query.get("services_id"), + status=query.get("status"), + limit=query.get("limit", 10), + offset=query.get("offset", 0), + asc_order=query.get("asc_order"), + ) assert ids_in_response == expected_note_ids # проверка корректности данных отфильтрованных модалок for resp_obj in response_data: - if type_id: assert resp_obj.get("type_id") == type_id - if modal_status: assert resp_obj.get("status") == modal_status + if type_id: + assert resp_obj.get("type_id") == type_id + if modal_status: + assert resp_obj.get("status") == modal_status @pytest.mark.parametrize( @@ -537,18 +542,74 @@ def test_get_note_by_id(client, notes, status_code, note_n, type_model): @pytest.mark.parametrize( "status_code, note_n, modal_status, deleted_group_id_flag, deleted_service_id_flag", [ - (status.HTTP_200_OK, 0, ModalStatus.ARCHIVED, False, False, ), - (status.HTTP_404_NOT_FOUND, -1, ModalStatus.ACTIVE, False, False, ), - (status.HTTP_200_OK, 3, ModalStatus.ACTIVE, False, False, ), - (status.HTTP_403_FORBIDDEN, 3, ModalStatus.ACTIVE, True, False, ), - (status.HTTP_403_FORBIDDEN, 3, ModalStatus.ACTIVE, False, True, ), - (status.HTTP_403_FORBIDDEN, 3, ModalStatus.ACTIVE, True, True, ), - (status.HTTP_403_FORBIDDEN, 6, ModalStatus.ACTIVE, False, False, ), # просроченная модалка - (status.HTTP_200_OK, 7, ModalStatus.ACTIVE, False, False, ), # просроченная модалка с is_always=True + ( + status.HTTP_200_OK, + 0, + ModalStatus.ARCHIVED, + False, + False, + ), + ( + status.HTTP_404_NOT_FOUND, + -1, + ModalStatus.ACTIVE, + False, + False, + ), + ( + status.HTTP_200_OK, + 3, + ModalStatus.ACTIVE, + False, + False, + ), + ( + status.HTTP_403_FORBIDDEN, + 3, + ModalStatus.ACTIVE, + True, + False, + ), + ( + status.HTTP_403_FORBIDDEN, + 3, + ModalStatus.ACTIVE, + False, + True, + ), + ( + status.HTTP_403_FORBIDDEN, + 3, + ModalStatus.ACTIVE, + True, + True, + ), + ( + status.HTTP_403_FORBIDDEN, + 6, + ModalStatus.ACTIVE, + False, + False, + ), # просроченная модалка + ( + status.HTTP_200_OK, + 7, + ModalStatus.ACTIVE, + False, + False, + ), # просроченная модалка с is_always=True ], ) def test_update_note_status( - client, dbsession, mock_datetime_now, notes, note_n, status_code, modal_status, deleted_group_id_flag, deleted_service_id_flag + client, + dbsession, + mock_datetime_now, + notes, + note_n, + status_code, + modal_status, + deleted_group_id_flag, + deleted_service_id_flag, ): notes_indexes = range(len(notes)) @@ -589,7 +650,5 @@ def test_delete_note(client, dbsession, notes, status_code, note_n): response = client.delete(f"{url}/{id_of_note}") assert response.status_code == status_code if status_code == status.HTTP_200_OK: - deleted_note = ( - dbsession.query(Note).filter(Note.id == id_of_note).populate_existing().one_or_none() - ) + deleted_note = dbsession.query(Note).filter(Note.id == id_of_note).populate_existing().one_or_none() assert deleted_note.is_deleted diff --git a/tests/test_routes/test_services.py b/tests/test_routes/test_services.py index b3fdc33..b324192 100644 --- a/tests/test_routes/test_services.py +++ b/tests/test_routes/test_services.py @@ -104,9 +104,18 @@ def test_update_service(client, dbsession, services, status_code, body, 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.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): From adec982d49ba86235d7b535531d7bf726a0a5b67 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Mon, 17 Aug 2026 20:46:34 +0300 Subject: [PATCH 13/14] =?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(=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BA=D0=B0=20=D0=B2=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80?= =?UTF-8?q?=D1=82=D0=B0=D1=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/versions/bd5dc5ad8627_changed_logic.py | 4 ++-- modal_backend/models/db.py | 1 - tests/conftest.py | 2 +- tests/test_routes/test_notes.py | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/migrations/versions/bd5dc5ad8627_changed_logic.py b/migrations/versions/bd5dc5ad8627_changed_logic.py index 3c1171f..3f203d2 100644 --- a/migrations/versions/bd5dc5ad8627_changed_logic.py +++ b/migrations/versions/bd5dc5ad8627_changed_logic.py @@ -5,9 +5,9 @@ Create Date: 2026-08-17 12:55:02.773293 """ -from alembic import op -import sqlalchemy as sa +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = 'bd5dc5ad8627' diff --git a/modal_backend/models/db.py b/modal_backend/models/db.py index efa666d..e05adcc 100644 --- a/modal_backend/models/db.py +++ b/modal_backend/models/db.py @@ -11,7 +11,6 @@ Integer, String, cast, - func, or_, true, ) diff --git a/tests/conftest.py b/tests/conftest.py index 7b861d4..8e5148f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer -from modal_backend.models.db import Group, ModalStatus, NoteTypeEnum, Note, Service +from modal_backend.models.db import Group, ModalStatus, Note, NoteTypeEnum, Service from modal_backend.schemas.models import ( NoteChoicePost, NoteImagePost, diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index 4afe59d..7189c15 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -3,7 +3,7 @@ import pytest from starlette import status -from modal_backend.models.db import Group, ModalStatus, NoteTypeEnum, Note, Service +from modal_backend.models.db import Group, ModalStatus, Note, NoteTypeEnum, Service from modal_backend.schemas.models import NoteChoiceGet, NoteImageGet, NoteInfoGet, NoteRatingGet, NoteTextGet from modal_backend.settings import get_settings From 09fd12efa3264dbe040c8e23ad9225371b3e4797 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Mon, 17 Aug 2026 22:33:41 +0300 Subject: [PATCH 14/14] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B5=D0=BA=D0=BE=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=B0=20Enum=20=D0=B2=20=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=20=D1=81=D1=8B=D1=80=D1=8B=D1=85=20=D0=B7=D0=BD?= =?UTF-8?q?=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_notes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_routes/test_notes.py b/tests/test_routes/test_notes.py index 7189c15..1f9ea39 100644 --- a/tests/test_routes/test_notes.py +++ b/tests/test_routes/test_notes.py @@ -265,7 +265,7 @@ def calculate_expected_len( None, # type_id [2], # group_n_list [2], # service_n_list - "active", # modal_status + ModalStatus.ACTIVE.value, # modal_status True, # asc_order 2, # limit 1, # offset @@ -275,7 +275,7 @@ def calculate_expected_len( None, # type_id [0, 1, 2], # group_n_list [0, 1, 2], # service_n_list - "archived", # modal_status + ModalStatus.ARCHIVED.value, # modal_status False, # asc_order 999, # limit 1, # offset @@ -285,7 +285,7 @@ def calculate_expected_len( None, # type_id [0, 1, 2], # group_n_list [0, 1, 2], # service_n_list - "archived", # modal_status + ModalStatus.ARCHIVED.value, # modal_status False, # asc_order None, # limit 5, # offset @@ -312,7 +312,7 @@ def calculate_expected_len( ), ( # ограничение по типу модалки status.HTTP_200_OK, - NoteTypeEnum.CHOICE, # type_id + NoteTypeEnum.CHOICE.value, # type_id None, # group_n_list None, # service_n_list None, # modal_status