- 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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
"""Pytest configuration & shared fixtures."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Regression tests ensuring Enum mappings store lowercase values."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
"""Simple in-memory Redis replacement for tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
|
||||
Reference in New Issue
Block a user