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()
+1
View File
@@ -1,4 +1,5 @@
"""Pytest configuration & shared fixtures."""
from __future__ import annotations
import sys
+1
View File
@@ -1,4 +1,5 @@
"""Regression tests ensuring Enum mappings store lowercase values."""
from __future__ import annotations
from enum import StrEnum
+17 -8
View File
@@ -1,14 +1,12 @@
"""Unit tests for ActivityService."""
from __future__ import annotations
from collections.abc import AsyncGenerator
import uuid
from collections.abc import AsyncGenerator
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
from app.models.activity import Activity, ActivityType
from app.models.base import Base
from app.models.contact import Contact
@@ -24,6 +22,8 @@ from app.services.activity_service import (
ActivityValidationError,
)
from app.services.organization_service import OrganizationContext
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
@pytest_asyncio.fixture()
@@ -91,9 +91,16 @@ async def test_list_activities_returns_only_current_deal(session: AsyncSession)
session.add_all(
[
Activity(deal_id=deal_id, author_id=context.user_id, type=ActivityType.COMMENT, payload={"text": "hi"}),
Activity(deal_id=deal_id + 1, author_id=context.user_id, type=ActivityType.SYSTEM, payload={}),
]
Activity(
deal_id=deal_id,
author_id=context.user_id,
type=ActivityType.COMMENT,
payload={"text": "hi"},
),
Activity(
deal_id=deal_id + 1, author_id=context.user_id, type=ActivityType.SYSTEM, payload={}
),
],
)
await session.flush()
@@ -112,7 +119,9 @@ async def test_add_comment_rejects_empty_text(session: AsyncSession) -> None:
service = ActivityService(repository=repo)
with pytest.raises(ActivityValidationError):
await service.add_comment(deal_id=deal_id, author_id=context.user_id, text=" ", context=context)
await service.add_comment(
deal_id=deal_id, author_id=context.user_id, text=" ", context=context
)
@pytest.mark.asyncio
+16 -8
View File
@@ -1,4 +1,5 @@
"""Unit tests for AnalyticsService."""
from __future__ import annotations
from collections.abc import AsyncGenerator
@@ -7,9 +8,6 @@ from decimal import Decimal
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
from app.models import Base
from app.models.contact import Contact
from app.models.deal import Deal, DealStage, DealStatus
@@ -18,13 +16,17 @@ from app.models.organization_member import OrganizationMember, OrganizationRole
from app.models.user import User
from app.repositories.analytics_repo import AnalyticsRepository
from app.services.analytics_service import AnalyticsService, invalidate_analytics_cache
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
from tests.utils.fake_redis import InMemoryRedis
@pytest_asyncio.fixture()
async def session() -> AsyncGenerator[AsyncSession, None]:
engine = create_async_engine(
"sqlite+aiosqlite:///:memory:", future=True, poolclass=StaticPool
"sqlite+aiosqlite:///:memory:",
future=True,
poolclass=StaticPool,
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
@@ -36,12 +38,18 @@ async def session() -> AsyncGenerator[AsyncSession, None]:
async def _seed_data(session: AsyncSession) -> tuple[int, int, int]:
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()
member = OrganizationMember(organization_id=org.id, user_id=user.id, role=OrganizationRole.OWNER)
contact = Contact(organization_id=org.id, owner_id=user.id, name="Client", email="client@example.com")
member = OrganizationMember(
organization_id=org.id, user_id=user.id, role=OrganizationRole.OWNER
)
contact = Contact(
organization_id=org.id, owner_id=user.id, name="Client", email="client@example.com"
)
session.add_all([member, contact])
await session.flush()
@@ -231,4 +239,4 @@ async def test_funnel_reads_from_cache_when_available(session: AsyncSession) ->
service._repository = _ExplodingRepository(session)
cached = await service.get_deal_funnel(org_id)
assert len(cached) == 4
assert len(cached) == 4
+11 -5
View File
@@ -1,16 +1,16 @@
"""Unit tests for AuthService."""
from __future__ import annotations
from typing import cast
from unittest.mock import MagicMock
import pytest # type: ignore[import-not-found]
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.security import JWTService, PasswordHasher
from app.models.user import User
from app.repositories.user_repo import UserRepository
from app.services.auth_service import AuthService, InvalidCredentialsError, InvalidRefreshTokenError
from sqlalchemy.ext.asyncio import AsyncSession
class StubUserRepository(UserRepository):
@@ -49,7 +49,9 @@ def jwt_service() -> JWTService:
@pytest.mark.asyncio
async def test_authenticate_success(password_hasher: PasswordHasher, jwt_service: JWTService) -> None:
async def test_authenticate_success(
password_hasher: PasswordHasher, jwt_service: JWTService
) -> None:
hashed = password_hasher.hash("StrongPass123")
user = User(email="user@example.com", hashed_password=hashed, name="Alice", is_active=True)
user.id = 1
@@ -100,7 +102,9 @@ async def test_refresh_tokens_returns_new_pair(
password_hasher: PasswordHasher,
jwt_service: JWTService,
) -> None:
user = User(email="refresh@example.com", hashed_password="hashed", name="Refresh", is_active=True)
user = User(
email="refresh@example.com", hashed_password="hashed", name="Refresh", is_active=True
)
user.id = 7
service = AuthService(StubUserRepository(user), password_hasher, jwt_service)
@@ -116,7 +120,9 @@ async def test_refresh_tokens_rejects_access_token(
password_hasher: PasswordHasher,
jwt_service: JWTService,
) -> None:
user = User(email="refresh@example.com", hashed_password="hashed", name="Refresh", is_active=True)
user = User(
email="refresh@example.com", hashed_password="hashed", name="Refresh", is_active=True
)
user.id = 9
service = AuthService(StubUserRepository(user), password_hasher, jwt_service)
+6 -6
View File
@@ -1,15 +1,12 @@
"""Unit tests for ContactService."""
from __future__ import annotations
from collections.abc import AsyncGenerator
import uuid
from collections.abc import AsyncGenerator
import pytest
import pytest_asyncio
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
from app.models.base import Base
from app.models.contact import Contact, ContactCreate
from app.models.deal import Deal
@@ -25,6 +22,9 @@ from app.services.contact_service import (
ContactUpdateData,
)
from app.services.organization_service import OrganizationContext
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
@pytest_asyncio.fixture()
@@ -244,7 +244,7 @@ async def test_delete_contact_blocks_when_deals_exist(session: AsyncSession) ->
owner_id=contact.owner_id,
title="Pending",
amount=None,
)
),
)
await session.flush()
+8 -6
View File
@@ -1,16 +1,13 @@
"""Unit tests for DealService."""
from __future__ import annotations
import uuid
from collections.abc import AsyncGenerator
from decimal import Decimal
import uuid
import pytest # type: ignore[import-not-found]
import pytest_asyncio # type: ignore[import-not-found]
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
from app.models.activity import Activity, ActivityType
from app.models.base import Base
from app.models.contact import Contact
@@ -28,6 +25,9 @@ from app.services.deal_service import (
DealUpdateData,
)
from app.services.organization_service import OrganizationContext
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
@pytest_asyncio.fixture()
@@ -64,7 +64,9 @@ def _make_context(org: Organization, user: User, role: OrganizationRole) -> Orga
return OrganizationContext(organization=org, membership=membership)
async def _persist_base(session: AsyncSession, *, role: OrganizationRole = OrganizationRole.MANAGER) -> tuple[
async def _persist_base(
session: AsyncSession, *, role: OrganizationRole = OrganizationRole.MANAGER
) -> tuple[
OrganizationContext,
Contact,
DealRepository,
+18 -8
View File
@@ -1,12 +1,11 @@
"""Unit tests for OrganizationService."""
from __future__ import annotations
from typing import cast
from unittest.mock import MagicMock
import pytest # type: ignore[import-not-found]
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.organization import Organization
from app.models.organization_member import OrganizationMember, OrganizationRole
from app.repositories.org_repo import OrganizationRepository
@@ -18,6 +17,7 @@ from app.services.organization_service import (
OrganizationMemberAlreadyExistsError,
OrganizationService,
)
from sqlalchemy.ext.asyncio import AsyncSession
class StubOrganizationRepository(OrganizationRepository):
@@ -27,7 +27,9 @@ class StubOrganizationRepository(OrganizationRepository):
super().__init__(session=MagicMock(spec=AsyncSession))
self._membership = membership
async def get_membership(self, organization_id: int, user_id: int) -> OrganizationMember | None: # pragma: no cover - helper
async def get_membership(
self, organization_id: int, user_id: int
) -> OrganizationMember | None: # pragma: no cover - helper
if (
self._membership
and self._membership.organization_id == organization_id
@@ -37,7 +39,9 @@ class StubOrganizationRepository(OrganizationRepository):
return None
def make_membership(role: OrganizationRole, *, organization_id: int = 1, user_id: int = 10) -> OrganizationMember:
def make_membership(
role: OrganizationRole, *, organization_id: int = 1, user_id: int = 10
) -> OrganizationMember:
organization = Organization(name="Acme Inc")
organization.id = organization_id
membership = OrganizationMember(
@@ -70,7 +74,9 @@ class SessionStub:
class MembershipRepositoryStub(OrganizationRepository):
"""Repository stub that can emulate duplicate checks for add_member."""
def __init__(self, memberships: dict[tuple[int, int], OrganizationMember] | None = None) -> None:
def __init__(
self, memberships: dict[tuple[int, int], OrganizationMember] | None = None
) -> None:
self._session_stub = SessionStub()
super().__init__(session=cast(AsyncSession, self._session_stub))
self._memberships = memberships or {}
@@ -88,7 +94,9 @@ async def test_get_context_success() -> None:
membership = make_membership(OrganizationRole.MANAGER)
service = OrganizationService(StubOrganizationRepository(membership))
context = await service.get_context(user_id=membership.user_id, organization_id=membership.organization_id)
context = await service.get_context(
user_id=membership.user_id, organization_id=membership.organization_id
)
assert context.organization_id == membership.organization_id
assert context.role == OrganizationRole.MANAGER
@@ -174,7 +182,9 @@ async def test_add_member_rejects_duplicate_membership() -> None:
service = OrganizationService(repo)
with pytest.raises(OrganizationMemberAlreadyExistsError):
await service.add_member(context=context, user_id=duplicate_user_id, role=OrganizationRole.MANAGER)
await service.add_member(
context=context, user_id=duplicate_user_id, role=OrganizationRole.MANAGER
)
@pytest.mark.asyncio
@@ -191,4 +201,4 @@ async def test_add_member_requires_privileged_role() -> None:
await service.add_member(context=context, user_id=99, role=OrganizationRole.MANAGER)
# Ensure DB work not attempted when permissions fail.
assert repo.session_stub.committed is False
assert repo.session_stub.committed is False
+8 -6
View File
@@ -1,16 +1,13 @@
"""Unit tests for TaskService."""
from __future__ import annotations
import uuid
from collections.abc import AsyncGenerator
from datetime import datetime, timedelta, timezone
import uuid
import pytest
import pytest_asyncio
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
from app.models.activity import Activity, ActivityType
from app.models.base import Base
from app.models.contact import Contact
@@ -28,6 +25,9 @@ from app.services.task_service import (
TaskService,
TaskUpdateData,
)
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
@pytest_asyncio.fixture()
@@ -189,7 +189,9 @@ async def test_member_cannot_update_foreign_task(session: AsyncSession) -> None:
user_id=member.id,
role=OrganizationRole.MEMBER,
)
member_context = OrganizationContext(organization=context_owner.organization, membership=membership)
member_context = OrganizationContext(
organization=context_owner.organization, membership=membership
)
with pytest.raises(TaskForbiddenError):
await service.update_task(
+1
View File
@@ -1,4 +1,5 @@
"""Simple in-memory Redis replacement for tests."""
from __future__ import annotations
import fnmatch