Refactor code for improved readability and consistency
Test / test (push) Successful in 15s

- Reformatted function signatures in `organization_service.py` and `task_service.py` for better alignment.
- Updated import statements across multiple files for consistency and organization.
- Enhanced test files by improving formatting and ensuring consistent use of async session factories.
- Added type hints and improved type safety in various service and test files.
- Adjusted `pyproject.toml` to include configuration for isort, mypy, and ruff for better code quality checks.
- Cleaned up unused imports and organized existing ones in several test files.
This commit is contained in:
Artem Kashaev
2025-12-01 16:18:03 +05:00
parent eecb74c523
commit 5fcb574aca
62 changed files with 765 additions and 476 deletions
+3 -3
View File
@@ -1,17 +1,17 @@
"""Pytest fixtures shared across API v1 tests."""
from __future__ import annotations
from collections.abc import AsyncGenerator
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.api.deps import get_cache_backend, get_db_session
from app.core.security import password_hasher
from app.main import create_app
from app.models import Base
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from tests.utils.fake_redis import InMemoryRedis
+5 -3
View File
@@ -1,17 +1,17 @@
"""Shared helpers for task and activity API tests."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.core.security import jwt_service
from app.models.contact import Contact
from app.models.deal import Deal
from app.models.organization import Organization
from app.models.organization_member import OrganizationMember, OrganizationRole
from app.models.user import User
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
@dataclass(slots=True)
@@ -27,7 +27,9 @@ class Scenario:
async def prepare_scenario(session_factory: async_sessionmaker[AsyncSession]) -> Scenario:
async with session_factory() as session:
user = User(email="owner@example.com", hashed_password="hashed", name="Owner", is_active=True)
user = User(
email="owner@example.com", hashed_password="hashed", name="Owner", is_active=True
)
org = Organization(name="Acme LLC")
session.add_all([user, org])
await session.flush()
+6 -5
View File
@@ -1,20 +1,20 @@
"""API tests for activity endpoints."""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
import pytest
from app.models.activity import Activity, ActivityType
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.models.activity import Activity, ActivityType
from tests.api.v1.task_activity_shared import auth_headers, make_token, prepare_scenario
@pytest.mark.asyncio
async def test_create_activity_comment_endpoint(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_scenario(session_factory)
token = make_token(scenario.user_id, scenario.user_email)
@@ -33,7 +33,8 @@ async def test_create_activity_comment_endpoint(
@pytest.mark.asyncio
async def test_list_activities_endpoint_supports_pagination(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_scenario(session_factory)
token = make_token(scenario.user_id, scenario.user_email)
+22 -11
View File
@@ -1,4 +1,5 @@
"""API tests for analytics endpoints."""
from __future__ import annotations
from dataclasses import dataclass
@@ -6,15 +7,14 @@ from datetime import datetime, timedelta, timezone
from decimal import Decimal
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.core.security import jwt_service
from app.models.contact import Contact
from app.models.deal import Deal, DealStage, DealStatus
from app.models.organization import Organization
from app.models.organization_member import OrganizationMember, OrganizationRole
from app.models.user import User
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
@dataclass(slots=True)
@@ -26,10 +26,14 @@ class AnalyticsScenario:
in_progress_deal_id: int
async def prepare_analytics_scenario(session_factory: async_sessionmaker[AsyncSession]) -> AnalyticsScenario:
async def prepare_analytics_scenario(
session_factory: async_sessionmaker[AsyncSession],
) -> AnalyticsScenario:
async with session_factory() as session:
org = Organization(name="Analytics Org")
user = User(email="analytics@example.com", hashed_password="hashed", name="Analyst", is_active=True)
user = User(
email="analytics@example.com", hashed_password="hashed", name="Analyst", is_active=True
)
session.add_all([org, user])
await session.flush()
@@ -103,7 +107,9 @@ async def prepare_analytics_scenario(session_factory: async_sessionmaker[AsyncSe
user_id=user.id,
user_email=user.email,
token=token,
in_progress_deal_id=next(deal.id for deal in deals if deal.status is DealStatus.IN_PROGRESS),
in_progress_deal_id=next(
deal.id for deal in deals if deal.status is DealStatus.IN_PROGRESS
),
)
@@ -113,7 +119,8 @@ def _headers(token: str, organization_id: int) -> dict[str, str]:
@pytest.mark.asyncio
async def test_deals_summary_endpoint_returns_metrics(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_analytics_scenario(session_factory)
@@ -134,7 +141,8 @@ async def test_deals_summary_endpoint_returns_metrics(
@pytest.mark.asyncio
async def test_deals_summary_respects_days_filter(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_analytics_scenario(session_factory)
@@ -150,7 +158,8 @@ async def test_deals_summary_respects_days_filter(
@pytest.mark.asyncio
async def test_deals_funnel_returns_breakdown(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_analytics_scenario(session_factory)
@@ -162,7 +171,9 @@ async def test_deals_funnel_returns_breakdown(
assert response.status_code == 200
payload = response.json()
assert len(payload["stages"]) == 4
qualification = next(item for item in payload["stages"] if item["stage"] == DealStage.QUALIFICATION.value)
qualification = next(
item for item in payload["stages"] if item["stage"] == DealStage.QUALIFICATION.value
)
assert qualification["total"] == 1
proposal = next(item for item in payload["stages"] if item["stage"] == DealStage.PROPOSAL.value)
assert proposal["conversion_to_next"] == 100.0
@@ -198,4 +209,4 @@ async def test_deal_update_invalidates_cached_summary(
)
assert refreshed.status_code == 200
payload = refreshed.json()
assert payload["won"]["count"] == 2
assert payload["won"]["count"] == 2
+7 -7
View File
@@ -1,15 +1,15 @@
"""API tests for authentication endpoints."""
from __future__ import annotations
import pytest
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.core.security import password_hasher
from app.models.organization import Organization
from app.models.organization_member import OrganizationMember, OrganizationRole
from app.models.user import User
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
@pytest.mark.asyncio
@@ -37,7 +37,7 @@ async def test_register_user_creates_organization_membership(
assert user is not None
organization = await session.scalar(
select(Organization).where(Organization.name == payload["organization_name"])
select(Organization).where(Organization.name == payload["organization_name"]),
)
assert organization is not None
@@ -45,7 +45,7 @@ async def test_register_user_creates_organization_membership(
select(OrganizationMember).where(
OrganizationMember.organization_id == organization.id,
OrganizationMember.user_id == user.id,
)
),
)
assert membership is not None
assert membership.role == OrganizationRole.OWNER
@@ -71,7 +71,7 @@ async def test_register_user_without_organization_succeeds(
assert user is not None
membership = await session.scalar(
select(OrganizationMember).where(OrganizationMember.user_id == user.id)
select(OrganizationMember).where(OrganizationMember.user_id == user.id),
)
assert membership is None
+26 -20
View File
@@ -1,21 +1,21 @@
"""API tests for contact endpoints."""
from __future__ import annotations
import pytest
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.models.contact import Contact
from app.models.organization_member import OrganizationMember, OrganizationRole
from app.models.user import User
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from tests.api.v1.task_activity_shared import auth_headers, make_token, prepare_scenario
@pytest.mark.asyncio
async def test_list_contacts_supports_search_and_pagination(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_scenario(session_factory)
token = make_token(scenario.user_id, scenario.user_email)
@@ -37,7 +37,7 @@ async def test_list_contacts_supports_search_and_pagination(
email="beta@example.com",
phone=None,
),
]
],
)
await session.commit()
@@ -54,7 +54,8 @@ async def test_list_contacts_supports_search_and_pagination(
@pytest.mark.asyncio
async def test_create_contact_returns_created_payload(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_scenario(session_factory)
token = make_token(scenario.user_id, scenario.user_email)
@@ -78,7 +79,8 @@ async def test_create_contact_returns_created_payload(
@pytest.mark.asyncio
async def test_member_cannot_assign_foreign_owner(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_scenario(session_factory)
token = make_token(scenario.user_id, scenario.user_email)
@@ -88,7 +90,7 @@ async def test_member_cannot_assign_foreign_owner(
select(OrganizationMember).where(
OrganizationMember.organization_id == scenario.organization_id,
OrganizationMember.user_id == scenario.user_id,
)
),
)
assert membership is not None
membership.role = OrganizationRole.MEMBER
@@ -107,7 +109,7 @@ async def test_member_cannot_assign_foreign_owner(
organization_id=scenario.organization_id,
user_id=other_user.id,
role=OrganizationRole.ADMIN,
)
),
)
await session.commit()
@@ -126,7 +128,8 @@ async def test_member_cannot_assign_foreign_owner(
@pytest.mark.asyncio
async def test_member_can_view_foreign_contacts(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_scenario(session_factory)
token = make_token(scenario.user_id, scenario.user_email)
@@ -136,7 +139,7 @@ async def test_member_can_view_foreign_contacts(
select(OrganizationMember).where(
OrganizationMember.organization_id == scenario.organization_id,
OrganizationMember.user_id == scenario.user_id,
)
),
)
assert membership is not None
membership.role = OrganizationRole.MEMBER
@@ -155,7 +158,7 @@ async def test_member_can_view_foreign_contacts(
organization_id=scenario.organization_id,
user_id=other_user.id,
role=OrganizationRole.MANAGER,
)
),
)
session.add(
@@ -165,7 +168,7 @@ async def test_member_can_view_foreign_contacts(
name="Foreign Owner",
email="foreign@example.com",
phone=None,
)
),
)
await session.commit()
@@ -181,7 +184,8 @@ async def test_member_can_view_foreign_contacts(
@pytest.mark.asyncio
async def test_member_patch_foreign_contact_forbidden(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_scenario(session_factory)
token = make_token(scenario.user_id, scenario.user_email)
@@ -191,7 +195,7 @@ async def test_member_patch_foreign_contact_forbidden(
select(OrganizationMember).where(
OrganizationMember.organization_id == scenario.organization_id,
OrganizationMember.user_id == scenario.user_id,
)
),
)
assert membership is not None
membership.role = OrganizationRole.MEMBER
@@ -210,7 +214,7 @@ async def test_member_patch_foreign_contact_forbidden(
organization_id=scenario.organization_id,
user_id=other_user.id,
role=OrganizationRole.MANAGER,
)
),
)
contact = Contact(
@@ -235,7 +239,8 @@ async def test_member_patch_foreign_contact_forbidden(
@pytest.mark.asyncio
async def test_patch_contact_updates_fields(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_scenario(session_factory)
token = make_token(scenario.user_id, scenario.user_email)
@@ -266,7 +271,8 @@ async def test_patch_contact_updates_fields(
@pytest.mark.asyncio
async def test_delete_contact_with_deals_returns_conflict(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_scenario(session_factory)
token = make_token(scenario.user_id, scenario.user_email)
+4 -5
View File
@@ -1,16 +1,15 @@
"""API tests for deal endpoints."""
from __future__ import annotations
from decimal import Decimal
import pytest
from app.models.activity import Activity, ActivityType
from app.models.deal import Deal, DealStage, DealStatus
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.models.activity import Activity, ActivityType
from app.models.deal import Deal, DealStage, DealStatus
from tests.api.v1.task_activity_shared import auth_headers, make_token, prepare_scenario
@@ -105,7 +104,7 @@ async def test_update_deal_endpoint_updates_stage_and_logs_activity(
async with session_factory() as session:
activity_types = await session.scalars(
select(Activity.type).where(Activity.deal_id == scenario.deal_id)
select(Activity.type).where(Activity.deal_id == scenario.deal_id),
)
collected = set(activity_types.all())
+37 -16
View File
@@ -1,16 +1,13 @@
"""API tests for organization endpoints."""
from __future__ import annotations
from collections.abc import AsyncGenerator, Sequence
from datetime import timedelta
from typing import AsyncGenerator, Sequence, cast
from typing import cast
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.schema import Table
from app.api.deps import get_db_session
from app.core.security import jwt_service
from app.main import create_app
@@ -18,6 +15,10 @@ from app.models import Base
from app.models.organization import Organization
from app.models.organization_member import OrganizationMember, OrganizationRole
from app.models.user import User
from httpx import ASGITransport, AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.schema import Table
@pytest_asyncio.fixture()
@@ -55,10 +56,13 @@ async def client(
@pytest.mark.asyncio
async def test_list_user_organizations_returns_memberships(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
async with session_factory() as session:
user = User(email="owner@example.com", hashed_password="hashed", name="Owner", is_active=True)
user = User(
email="owner@example.com", hashed_password="hashed", name="Owner", is_active=True
)
session.add(user)
await session.flush()
@@ -110,8 +114,12 @@ async def test_owner_can_add_member_to_organization(
client: AsyncClient,
) -> None:
async with session_factory() as session:
owner = User(email="owner-add@example.com", hashed_password="hashed", name="Owner", is_active=True)
invitee = User(email="new-member@example.com", hashed_password="hashed", name="Member", is_active=True)
owner = User(
email="owner-add@example.com", hashed_password="hashed", name="Owner", is_active=True
)
invitee = User(
email="new-member@example.com", hashed_password="hashed", name="Member", is_active=True
)
session.add_all([owner, invitee])
await session.flush()
@@ -153,7 +161,7 @@ async def test_owner_can_add_member_to_organization(
select(OrganizationMember).where(
OrganizationMember.organization_id == organization.id,
OrganizationMember.user_id == invitee.id,
)
),
)
assert new_membership is not None
assert new_membership.role == OrganizationRole.MANAGER
@@ -165,7 +173,12 @@ async def test_add_member_requires_existing_user(
client: AsyncClient,
) -> None:
async with session_factory() as session:
owner = User(email="owner-missing@example.com", hashed_password="hashed", name="Owner", is_active=True)
owner = User(
email="owner-missing@example.com",
hashed_password="hashed",
name="Owner",
is_active=True,
)
session.add(owner)
await session.flush()
@@ -206,8 +219,12 @@ async def test_member_role_cannot_add_users(
client: AsyncClient,
) -> None:
async with session_factory() as session:
member_user = User(email="member@example.com", hashed_password="hashed", name="Member", is_active=True)
invitee = User(email="invitee@example.com", hashed_password="hashed", name="Invitee", is_active=True)
member_user = User(
email="member@example.com", hashed_password="hashed", name="Member", is_active=True
)
invitee = User(
email="invitee@example.com", hashed_password="hashed", name="Invitee", is_active=True
)
session.add_all([member_user, invitee])
await session.flush()
@@ -248,8 +265,12 @@ async def test_cannot_add_duplicate_member(
client: AsyncClient,
) -> None:
async with session_factory() as session:
owner = User(email="dup-owner@example.com", hashed_password="hashed", name="Owner", is_active=True)
invitee = User(email="dup-member@example.com", hashed_password="hashed", name="Invitee", is_active=True)
owner = User(
email="dup-owner@example.com", hashed_password="hashed", name="Owner", is_active=True
)
invitee = User(
email="dup-member@example.com", hashed_password="hashed", name="Invitee", is_active=True
)
session.add_all([owner, invitee])
await session.flush()
+13 -7
View File
@@ -1,20 +1,25 @@
"""API tests for task endpoints."""
from __future__ import annotations
from datetime import date, datetime, timedelta, timezone
import pytest
from app.models.task import Task
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.models.task import Task
from tests.api.v1.task_activity_shared import auth_headers, create_deal, make_token, prepare_scenario
from tests.api.v1.task_activity_shared import (
auth_headers,
create_deal,
make_token,
prepare_scenario,
)
@pytest.mark.asyncio
async def test_create_task_endpoint_creates_task_and_activity(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_scenario(session_factory)
token = make_token(scenario.user_id, scenario.user_email)
@@ -40,7 +45,8 @@ async def test_create_task_endpoint_creates_task_and_activity(
@pytest.mark.asyncio
async def test_list_tasks_endpoint_filters_by_deal(
session_factory: async_sessionmaker[AsyncSession], client: AsyncClient
session_factory: async_sessionmaker[AsyncSession],
client: AsyncClient,
) -> None:
scenario = await prepare_scenario(session_factory)
token = make_token(scenario.user_id, scenario.user_email)
@@ -63,7 +69,7 @@ async def test_list_tasks_endpoint_filters_by_deal(
due_date=datetime.now(timezone.utc) + timedelta(days=3),
is_done=False,
),
]
],
)
await session.commit()