refactor: improve code readability by formatting arguments across multiple files
Test / test (push) Successful in 15s
Details
Test / test (push) Successful in 15s
Details
This commit is contained in:
parent
dc0046c730
commit
1dd7c2f2b8
|
|
@ -83,7 +83,8 @@ async def create_contact(
|
||||||
service: ContactService = Depends(get_contact_service),
|
service: ContactService = Depends(get_contact_service),
|
||||||
) -> ContactRead:
|
) -> ContactRead:
|
||||||
data = payload.to_domain(
|
data = payload.to_domain(
|
||||||
organization_id=context.organization_id, fallback_owner=context.user_id,
|
organization_id=context.organization_id,
|
||||||
|
fallback_owner=context.user_id,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
contact = await service.create_contact(data, context=context)
|
contact = await service.create_contact(data, context=context)
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,8 @@ async def list_deals(
|
||||||
stage_value = DealStage(stage) if stage else None
|
stage_value = DealStage(stage) if stage else None
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid deal filter",
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid deal filter",
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
params = DealQueryParams(
|
params = DealQueryParams(
|
||||||
|
|
@ -100,7 +101,8 @@ async def create_deal(
|
||||||
"""Create a new deal within the current organization."""
|
"""Create a new deal within the current organization."""
|
||||||
|
|
||||||
data = payload.to_domain(
|
data = payload.to_domain(
|
||||||
organization_id=context.organization_id, fallback_owner=context.user_id,
|
organization_id=context.organization_id,
|
||||||
|
fallback_owner=context.user_id,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
deal = await service.create_deal(data, context=context)
|
deal = await service.create_deal(data, context=context)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,9 @@ class RedisCacheManager:
|
||||||
if self._client is not None:
|
if self._client is not None:
|
||||||
return
|
return
|
||||||
self._client = redis.from_url(
|
self._client = redis.from_url(
|
||||||
settings.redis_url, encoding="utf-8", decode_responses=False,
|
settings.redis_url,
|
||||||
|
encoding="utf-8",
|
||||||
|
decode_responses=False,
|
||||||
)
|
)
|
||||||
await self._refresh_availability()
|
await self._refresh_availability()
|
||||||
|
|
||||||
|
|
@ -63,7 +65,9 @@ class RedisCacheManager:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = redis.from_url(
|
self._client = redis.from_url(
|
||||||
settings.redis_url, encoding="utf-8", decode_responses=False,
|
settings.redis_url,
|
||||||
|
encoding="utf-8",
|
||||||
|
decode_responses=False,
|
||||||
)
|
)
|
||||||
await self._refresh_availability()
|
await self._refresh_availability()
|
||||||
|
|
||||||
|
|
@ -127,7 +131,11 @@ async def read_json(client: Redis, key: str) -> Any | None:
|
||||||
|
|
||||||
|
|
||||||
async def write_json(
|
async def write_json(
|
||||||
client: Redis, key: str, value: Any, ttl_seconds: int, backoff_ms: int,
|
client: Redis,
|
||||||
|
key: str,
|
||||||
|
value: Any,
|
||||||
|
ttl_seconds: int,
|
||||||
|
backoff_ms: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Serialize data to JSON and store it with TTL using retry/backoff."""
|
"""Serialize data to JSON and store it with TTL using retry/backoff."""
|
||||||
payload = json.dumps(value, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
|
payload = json.dumps(value, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ class Settings(BaseSettings):
|
||||||
db_name: str = Field(default="test_task_crm", description="Database name")
|
db_name: str = Field(default="test_task_crm", description="Database name")
|
||||||
db_user: str = Field(default="postgres", description="Database user")
|
db_user: str = Field(default="postgres", description="Database user")
|
||||||
db_password: SecretStr = Field(
|
db_password: SecretStr = Field(
|
||||||
default=SecretStr("postgres"), description="Database user password",
|
default=SecretStr("postgres"),
|
||||||
|
description="Database user password",
|
||||||
)
|
)
|
||||||
database_url_override: str | None = Field(
|
database_url_override: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
|
|
@ -32,7 +33,9 @@ class Settings(BaseSettings):
|
||||||
redis_enabled: bool = Field(default=False, description="Toggle Redis-backed cache usage")
|
redis_enabled: bool = Field(default=False, description="Toggle Redis-backed cache usage")
|
||||||
redis_url: str = Field(default="redis://localhost:6379/0", description="Redis connection URL")
|
redis_url: str = Field(default="redis://localhost:6379/0", description="Redis connection URL")
|
||||||
analytics_cache_ttl_seconds: int = Field(
|
analytics_cache_ttl_seconds: int = Field(
|
||||||
default=120, ge=1, description="TTL for cached analytics responses",
|
default=120,
|
||||||
|
ge=1,
|
||||||
|
description="TTL for cached analytics responses",
|
||||||
)
|
)
|
||||||
analytics_cache_backoff_ms: int = Field(
|
analytics_cache_backoff_ms: int = Field(
|
||||||
default=200,
|
default=200,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@ class User(Base):
|
||||||
)
|
)
|
||||||
|
|
||||||
memberships = relationship(
|
memberships = relationship(
|
||||||
"OrganizationMember", back_populates="user", cascade="all, delete-orphan",
|
"OrganizationMember",
|
||||||
|
back_populates="user",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
)
|
)
|
||||||
owned_contacts = relationship("Contact", back_populates="owner")
|
owned_contacts = relationship("Contact", back_populates="owner")
|
||||||
owned_deals = relationship("Deal", back_populates="owner")
|
owned_deals = relationship("Deal", back_populates="owner")
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ class ActivityRepository:
|
||||||
select(Activity)
|
select(Activity)
|
||||||
.join(Deal, Deal.id == Activity.deal_id)
|
.join(Deal, Deal.id == Activity.deal_id)
|
||||||
.where(
|
.where(
|
||||||
Activity.deal_id == params.deal_id, Deal.organization_id == params.organization_id,
|
Activity.deal_id == params.deal_id,
|
||||||
|
Deal.organization_id == params.organization_id,
|
||||||
)
|
)
|
||||||
.order_by(Activity.created_at)
|
.order_by(Activity.created_at)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,8 @@ class ContactRepository:
|
||||||
user_id: int,
|
user_id: int,
|
||||||
) -> Contact | None:
|
) -> Contact | None:
|
||||||
stmt = select(Contact).where(
|
stmt = select(Contact).where(
|
||||||
Contact.id == contact_id, Contact.organization_id == organization_id,
|
Contact.id == contact_id,
|
||||||
|
Contact.organization_id == organization_id,
|
||||||
)
|
)
|
||||||
result = await self._session.scalars(stmt)
|
result = await self._session.scalars(stmt)
|
||||||
return result.first()
|
return result.first()
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,9 @@ class DealRepository:
|
||||||
return stmt
|
return stmt
|
||||||
|
|
||||||
def _apply_ordering(
|
def _apply_ordering(
|
||||||
self, stmt: Select[tuple[Deal]], params: DealQueryParams,
|
self,
|
||||||
|
stmt: Select[tuple[Deal]],
|
||||||
|
params: DealQueryParams,
|
||||||
) -> Select[tuple[Deal]]:
|
) -> Select[tuple[Deal]]:
|
||||||
column = ORDERABLE_COLUMNS.get(params.order_by or "created_at", Deal.created_at)
|
column = ORDERABLE_COLUMNS.get(params.order_by or "created_at", Deal.created_at)
|
||||||
order_func = desc if params.order_desc else asc
|
order_func = desc if params.order_desc else asc
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,9 @@ class TaskRepository:
|
||||||
return task
|
return task
|
||||||
|
|
||||||
def _apply_filters(
|
def _apply_filters(
|
||||||
self, stmt: Select[tuple[Task]], params: TaskQueryParams,
|
self,
|
||||||
|
stmt: Select[tuple[Task]],
|
||||||
|
params: TaskQueryParams,
|
||||||
) -> Select[tuple[Task]]:
|
) -> Select[tuple[Task]]:
|
||||||
if params.deal_id is not None:
|
if params.deal_id is not None:
|
||||||
stmt = stmt.where(Task.deal_id == params.deal_id)
|
stmt = stmt.where(Task.deal_id == params.deal_id)
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,10 @@ class AnalyticsService:
|
||||||
return _deserialize_summary(payload)
|
return _deserialize_summary(payload)
|
||||||
|
|
||||||
async def _store_summary_cache(
|
async def _store_summary_cache(
|
||||||
self, organization_id: int, days: int, summary: DealSummary,
|
self,
|
||||||
|
organization_id: int,
|
||||||
|
days: int,
|
||||||
|
summary: DealSummary,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not self._is_cache_enabled() or self._cache is None:
|
if not self._is_cache_enabled() or self._cache is None:
|
||||||
return
|
return
|
||||||
|
|
@ -187,7 +190,9 @@ class AnalyticsService:
|
||||||
return _deserialize_funnel(payload)
|
return _deserialize_funnel(payload)
|
||||||
|
|
||||||
async def _store_funnel_cache(
|
async def _store_funnel_cache(
|
||||||
self, organization_id: int, breakdowns: list[StageBreakdown],
|
self,
|
||||||
|
organization_id: int,
|
||||||
|
breakdowns: list[StageBreakdown],
|
||||||
) -> None:
|
) -> None:
|
||||||
if not self._is_cache_enabled() or self._cache is None:
|
if not self._is_cache_enabled() or self._cache is None:
|
||||||
return
|
return
|
||||||
|
|
@ -321,7 +326,9 @@ def _deserialize_funnel(payload: Any) -> list[StageBreakdown] | None:
|
||||||
|
|
||||||
|
|
||||||
async def invalidate_analytics_cache(
|
async def invalidate_analytics_cache(
|
||||||
cache: Redis | None, organization_id: int, backoff_ms: int,
|
cache: Redis | None,
|
||||||
|
organization_id: int,
|
||||||
|
backoff_ms: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Remove cached analytics payloads for the organization."""
|
"""Remove cached analytics payloads for the organization."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,9 @@ class ContactService:
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return await self._repository.list(
|
return await self._repository.list(
|
||||||
params=params, role=context.role, user_id=context.user_id,
|
params=params,
|
||||||
|
role=context.role,
|
||||||
|
user_id=context.user_id,
|
||||||
)
|
)
|
||||||
except ContactAccessError as exc:
|
except ContactAccessError as exc:
|
||||||
raise ContactForbiddenError(str(exc)) from exc
|
raise ContactForbiddenError(str(exc)) from exc
|
||||||
|
|
@ -126,7 +128,10 @@ class ContactService:
|
||||||
return contact
|
return contact
|
||||||
try:
|
try:
|
||||||
return await self._repository.update(
|
return await self._repository.update(
|
||||||
contact, payload, role=context.role, user_id=context.user_id,
|
contact,
|
||||||
|
payload,
|
||||||
|
role=context.role,
|
||||||
|
user_id=context.user_id,
|
||||||
)
|
)
|
||||||
except ContactAccessError as exc:
|
except ContactAccessError as exc:
|
||||||
raise ContactForbiddenError(str(exc)) from exc
|
raise ContactForbiddenError(str(exc)) from exc
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,9 @@ class DealService:
|
||||||
await self._ensure_contact_in_organization(data.contact_id, context.organization_id)
|
await self._ensure_contact_in_organization(data.contact_id, context.organization_id)
|
||||||
deal = await self._repository.create(data=data, role=context.role, user_id=context.user_id)
|
deal = await self._repository.create(data=data, role=context.role, user_id=context.user_id)
|
||||||
await invalidate_analytics_cache(
|
await invalidate_analytics_cache(
|
||||||
self._cache, context.organization_id, self._cache_backoff_ms,
|
self._cache,
|
||||||
|
context.organization_id,
|
||||||
|
self._cache_backoff_ms,
|
||||||
)
|
)
|
||||||
return deal
|
return deal
|
||||||
|
|
||||||
|
|
@ -120,7 +122,10 @@ class DealService:
|
||||||
return deal
|
return deal
|
||||||
|
|
||||||
updated = await self._repository.update(
|
updated = await self._repository.update(
|
||||||
deal, changes, role=context.role, user_id=context.user_id,
|
deal,
|
||||||
|
changes,
|
||||||
|
role=context.role,
|
||||||
|
user_id=context.user_id,
|
||||||
)
|
)
|
||||||
await self._log_activities(
|
await self._log_activities(
|
||||||
deal_id=deal.id,
|
deal_id=deal.id,
|
||||||
|
|
@ -128,7 +133,9 @@ class DealService:
|
||||||
activities=[activity for activity in [stage_activity, status_activity] if activity],
|
activities=[activity for activity in [stage_activity, status_activity] if activity],
|
||||||
)
|
)
|
||||||
await invalidate_analytics_cache(
|
await invalidate_analytics_cache(
|
||||||
self._cache, context.organization_id, self._cache_backoff_ms,
|
self._cache,
|
||||||
|
context.organization_id,
|
||||||
|
self._cache_backoff_ms,
|
||||||
)
|
)
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|
@ -150,7 +157,10 @@ class DealService:
|
||||||
return
|
return
|
||||||
for activity_type, payload in entries:
|
for activity_type, payload in entries:
|
||||||
activity = Activity(
|
activity = Activity(
|
||||||
deal_id=deal_id, author_id=author_id, type=activity_type, payload=payload,
|
deal_id=deal_id,
|
||||||
|
author_id=author_id,
|
||||||
|
type=activity_type,
|
||||||
|
payload=payload,
|
||||||
)
|
)
|
||||||
self._repository.session.add(activity)
|
self._repository.session.add(activity)
|
||||||
await self._repository.session.flush()
|
await self._repository.session.flush()
|
||||||
|
|
@ -160,7 +170,9 @@ class DealService:
|
||||||
raise DealOrganizationMismatchError("Operation targets a different organization")
|
raise DealOrganizationMismatchError("Operation targets a different organization")
|
||||||
|
|
||||||
async def _ensure_contact_in_organization(
|
async def _ensure_contact_in_organization(
|
||||||
self, contact_id: int, organization_id: int,
|
self,
|
||||||
|
contact_id: int,
|
||||||
|
organization_id: int,
|
||||||
) -> Contact:
|
) -> Contact:
|
||||||
contact = await self._repository.session.get(Contact, contact_id)
|
contact = await self._repository.session.get(Contact, contact_id)
|
||||||
if contact is None or contact.organization_id != organization_id:
|
if contact is None or contact.organization_id != organization_id:
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,10 @@ class OrganizationService:
|
||||||
self._repository = repository
|
self._repository = repository
|
||||||
|
|
||||||
async def get_context(
|
async def get_context(
|
||||||
self, *, user_id: int, organization_id: int | None,
|
self,
|
||||||
|
*,
|
||||||
|
user_id: int,
|
||||||
|
organization_id: int | None,
|
||||||
) -> OrganizationContext:
|
) -> OrganizationContext:
|
||||||
"""Resolve request context ensuring the user belongs to the given organization."""
|
"""Resolve request context ensuring the user belongs to the given organization."""
|
||||||
|
|
||||||
|
|
@ -70,7 +73,10 @@ class OrganizationService:
|
||||||
return OrganizationContext(organization=membership.organization, membership=membership)
|
return OrganizationContext(organization=membership.organization, membership=membership)
|
||||||
|
|
||||||
def ensure_entity_in_context(
|
def ensure_entity_in_context(
|
||||||
self, *, entity_organization_id: int, context: OrganizationContext,
|
self,
|
||||||
|
*,
|
||||||
|
entity_organization_id: int,
|
||||||
|
context: OrganizationContext,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Make sure a resource belongs to the current organization."""
|
"""Make sure a resource belongs to the current organization."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,10 @@ class Scenario:
|
||||||
async def prepare_scenario(session_factory: async_sessionmaker[AsyncSession]) -> Scenario:
|
async def prepare_scenario(session_factory: async_sessionmaker[AsyncSession]) -> Scenario:
|
||||||
async with session_factory() as session:
|
async with session_factory() as session:
|
||||||
user = User(
|
user = User(
|
||||||
email="owner@example.com", hashed_password="hashed", name="Owner", is_active=True,
|
email="owner@example.com",
|
||||||
|
hashed_password="hashed",
|
||||||
|
name="Owner",
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
org = Organization(name="Acme LLC")
|
org = Organization(name="Acme LLC")
|
||||||
session.add_all([user, org])
|
session.add_all([user, org])
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,10 @@ async def prepare_analytics_scenario(
|
||||||
async with session_factory() as session:
|
async with session_factory() as session:
|
||||||
org = Organization(name="Analytics Org")
|
org = Organization(name="Analytics Org")
|
||||||
user = User(
|
user = User(
|
||||||
email="analytics@example.com", hashed_password="hashed", name="Analyst", is_active=True,
|
email="analytics@example.com",
|
||||||
|
hashed_password="hashed",
|
||||||
|
name="Analyst",
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
session.add_all([org, user])
|
session.add_all([org, user])
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,10 @@ async def test_list_user_organizations_returns_memberships(
|
||||||
) -> None:
|
) -> None:
|
||||||
async with session_factory() as session:
|
async with session_factory() as session:
|
||||||
user = User(
|
user = User(
|
||||||
email="owner@example.com", hashed_password="hashed", name="Owner", is_active=True,
|
email="owner@example.com",
|
||||||
|
hashed_password="hashed",
|
||||||
|
name="Owner",
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
@ -115,10 +118,16 @@ async def test_owner_can_add_member_to_organization(
|
||||||
) -> None:
|
) -> None:
|
||||||
async with session_factory() as session:
|
async with session_factory() as session:
|
||||||
owner = User(
|
owner = User(
|
||||||
email="owner-add@example.com", hashed_password="hashed", name="Owner", is_active=True,
|
email="owner-add@example.com",
|
||||||
|
hashed_password="hashed",
|
||||||
|
name="Owner",
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
invitee = User(
|
invitee = User(
|
||||||
email="new-member@example.com", hashed_password="hashed", name="Member", is_active=True,
|
email="new-member@example.com",
|
||||||
|
hashed_password="hashed",
|
||||||
|
name="Member",
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
session.add_all([owner, invitee])
|
session.add_all([owner, invitee])
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
@ -220,10 +229,16 @@ async def test_member_role_cannot_add_users(
|
||||||
) -> None:
|
) -> None:
|
||||||
async with session_factory() as session:
|
async with session_factory() as session:
|
||||||
member_user = User(
|
member_user = User(
|
||||||
email="member@example.com", hashed_password="hashed", name="Member", is_active=True,
|
email="member@example.com",
|
||||||
|
hashed_password="hashed",
|
||||||
|
name="Member",
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
invitee = User(
|
invitee = User(
|
||||||
email="invitee@example.com", hashed_password="hashed", name="Invitee", is_active=True,
|
email="invitee@example.com",
|
||||||
|
hashed_password="hashed",
|
||||||
|
name="Invitee",
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
session.add_all([member_user, invitee])
|
session.add_all([member_user, invitee])
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
@ -266,10 +281,16 @@ async def test_cannot_add_duplicate_member(
|
||||||
) -> None:
|
) -> None:
|
||||||
async with session_factory() as session:
|
async with session_factory() as session:
|
||||||
owner = User(
|
owner = User(
|
||||||
email="dup-owner@example.com", hashed_password="hashed", name="Owner", is_active=True,
|
email="dup-owner@example.com",
|
||||||
|
hashed_password="hashed",
|
||||||
|
name="Owner",
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
invitee = User(
|
invitee = User(
|
||||||
email="dup-member@example.com", hashed_password="hashed", name="Invitee", is_active=True,
|
email="dup-member@example.com",
|
||||||
|
hashed_password="hashed",
|
||||||
|
name="Invitee",
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
session.add_all([owner, invitee])
|
session.add_all([owner, invitee])
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,10 @@ async def test_list_activities_returns_only_current_deal(session: AsyncSession)
|
||||||
payload={"text": "hi"},
|
payload={"text": "hi"},
|
||||||
),
|
),
|
||||||
Activity(
|
Activity(
|
||||||
deal_id=deal_id + 1, author_id=context.user_id, type=ActivityType.SYSTEM, payload={},
|
deal_id=deal_id + 1,
|
||||||
|
author_id=context.user_id,
|
||||||
|
type=ActivityType.SYSTEM,
|
||||||
|
payload={},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -120,7 +123,10 @@ async def test_add_comment_rejects_empty_text(session: AsyncSession) -> None:
|
||||||
|
|
||||||
with pytest.raises(ActivityValidationError):
|
with pytest.raises(ActivityValidationError):
|
||||||
await service.add_comment(
|
await service.add_comment(
|
||||||
deal_id=deal_id, author_id=context.user_id, text=" ", context=context,
|
deal_id=deal_id,
|
||||||
|
author_id=context.user_id,
|
||||||
|
text=" ",
|
||||||
|
context=context,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,16 +39,24 @@ async def session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
async def _seed_data(session: AsyncSession) -> tuple[int, int, int]:
|
async def _seed_data(session: AsyncSession) -> tuple[int, int, int]:
|
||||||
org = Organization(name="Analytics Org")
|
org = Organization(name="Analytics Org")
|
||||||
user = User(
|
user = User(
|
||||||
email="analytics@example.com", hashed_password="hashed", name="Analyst", is_active=True,
|
email="analytics@example.com",
|
||||||
|
hashed_password="hashed",
|
||||||
|
name="Analyst",
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
session.add_all([org, user])
|
session.add_all([org, user])
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
||||||
member = OrganizationMember(
|
member = OrganizationMember(
|
||||||
organization_id=org.id, user_id=user.id, role=OrganizationRole.OWNER,
|
organization_id=org.id,
|
||||||
|
user_id=user.id,
|
||||||
|
role=OrganizationRole.OWNER,
|
||||||
)
|
)
|
||||||
contact = Contact(
|
contact = Contact(
|
||||||
organization_id=org.id, owner_id=user.id, name="Client", email="client@example.com",
|
organization_id=org.id,
|
||||||
|
owner_id=user.id,
|
||||||
|
name="Client",
|
||||||
|
email="client@example.com",
|
||||||
)
|
)
|
||||||
session.add_all([member, contact])
|
session.add_all([member, contact])
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ def jwt_service() -> JWTService:
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_authenticate_success(
|
async def test_authenticate_success(
|
||||||
password_hasher: PasswordHasher, jwt_service: JWTService,
|
password_hasher: PasswordHasher,
|
||||||
|
jwt_service: JWTService,
|
||||||
) -> None:
|
) -> None:
|
||||||
hashed = password_hasher.hash("StrongPass123")
|
hashed = password_hasher.hash("StrongPass123")
|
||||||
user = User(email="user@example.com", hashed_password=hashed, name="Alice", is_active=True)
|
user = User(email="user@example.com", hashed_password=hashed, name="Alice", is_active=True)
|
||||||
|
|
@ -103,7 +104,10 @@ async def test_refresh_tokens_returns_new_pair(
|
||||||
jwt_service: JWTService,
|
jwt_service: JWTService,
|
||||||
) -> None:
|
) -> None:
|
||||||
user = User(
|
user = User(
|
||||||
email="refresh@example.com", hashed_password="hashed", name="Refresh", is_active=True,
|
email="refresh@example.com",
|
||||||
|
hashed_password="hashed",
|
||||||
|
name="Refresh",
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
user.id = 7
|
user.id = 7
|
||||||
service = AuthService(StubUserRepository(user), password_hasher, jwt_service)
|
service = AuthService(StubUserRepository(user), password_hasher, jwt_service)
|
||||||
|
|
@ -121,7 +125,10 @@ async def test_refresh_tokens_rejects_access_token(
|
||||||
jwt_service: JWTService,
|
jwt_service: JWTService,
|
||||||
) -> None:
|
) -> None:
|
||||||
user = User(
|
user = User(
|
||||||
email="refresh@example.com", hashed_password="hashed", name="Refresh", is_active=True,
|
email="refresh@example.com",
|
||||||
|
hashed_password="hashed",
|
||||||
|
name="Refresh",
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
user.id = 9
|
user.id = 9
|
||||||
service = AuthService(StubUserRepository(user), password_hasher, jwt_service)
|
service = AuthService(StubUserRepository(user), password_hasher, jwt_service)
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,9 @@ def _make_context(org: Organization, user: User, role: OrganizationRole) -> Orga
|
||||||
|
|
||||||
|
|
||||||
async def _persist_base(
|
async def _persist_base(
|
||||||
session: AsyncSession, *, role: OrganizationRole = OrganizationRole.MANAGER,
|
session: AsyncSession,
|
||||||
|
*,
|
||||||
|
role: OrganizationRole = OrganizationRole.MANAGER,
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
OrganizationContext,
|
OrganizationContext,
|
||||||
Contact,
|
Contact,
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ class StubOrganizationRepository(OrganizationRepository):
|
||||||
self._membership = membership
|
self._membership = membership
|
||||||
|
|
||||||
async def get_membership(
|
async def get_membership(
|
||||||
self, organization_id: int, user_id: int,
|
self,
|
||||||
|
organization_id: int,
|
||||||
|
user_id: int,
|
||||||
) -> OrganizationMember | None: # pragma: no cover - helper
|
) -> OrganizationMember | None: # pragma: no cover - helper
|
||||||
if (
|
if (
|
||||||
self._membership
|
self._membership
|
||||||
|
|
@ -40,7 +42,10 @@ class StubOrganizationRepository(OrganizationRepository):
|
||||||
|
|
||||||
|
|
||||||
def make_membership(
|
def make_membership(
|
||||||
role: OrganizationRole, *, organization_id: int = 1, user_id: int = 10,
|
role: OrganizationRole,
|
||||||
|
*,
|
||||||
|
organization_id: int = 1,
|
||||||
|
user_id: int = 10,
|
||||||
) -> OrganizationMember:
|
) -> OrganizationMember:
|
||||||
organization = Organization(name="Acme Inc")
|
organization = Organization(name="Acme Inc")
|
||||||
organization.id = organization_id
|
organization.id = organization_id
|
||||||
|
|
@ -75,7 +80,8 @@ class MembershipRepositoryStub(OrganizationRepository):
|
||||||
"""Repository stub that can emulate duplicate checks for add_member."""
|
"""Repository stub that can emulate duplicate checks for add_member."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, memberships: dict[tuple[int, int], OrganizationMember] | None = None,
|
self,
|
||||||
|
memberships: dict[tuple[int, int], OrganizationMember] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._session_stub = SessionStub()
|
self._session_stub = SessionStub()
|
||||||
super().__init__(session=cast(AsyncSession, self._session_stub))
|
super().__init__(session=cast(AsyncSession, self._session_stub))
|
||||||
|
|
@ -95,7 +101,8 @@ async def test_get_context_success() -> None:
|
||||||
service = OrganizationService(StubOrganizationRepository(membership))
|
service = OrganizationService(StubOrganizationRepository(membership))
|
||||||
|
|
||||||
context = await service.get_context(
|
context = await service.get_context(
|
||||||
user_id=membership.user_id, organization_id=membership.organization_id,
|
user_id=membership.user_id,
|
||||||
|
organization_id=membership.organization_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert context.organization_id == membership.organization_id
|
assert context.organization_id == membership.organization_id
|
||||||
|
|
@ -183,7 +190,9 @@ async def test_add_member_rejects_duplicate_membership() -> None:
|
||||||
|
|
||||||
with pytest.raises(OrganizationMemberAlreadyExistsError):
|
with pytest.raises(OrganizationMemberAlreadyExistsError):
|
||||||
await service.add_member(
|
await service.add_member(
|
||||||
context=context, user_id=duplicate_user_id, role=OrganizationRole.MANAGER,
|
context=context,
|
||||||
|
user_id=duplicate_user_id,
|
||||||
|
role=OrganizationRole.MANAGER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,8 @@ async def test_member_cannot_update_foreign_task(session: AsyncSession) -> None:
|
||||||
role=OrganizationRole.MEMBER,
|
role=OrganizationRole.MEMBER,
|
||||||
)
|
)
|
||||||
member_context = OrganizationContext(
|
member_context = OrganizationContext(
|
||||||
organization=context_owner.organization, membership=membership,
|
organization=context_owner.organization,
|
||||||
|
membership=membership,
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(TaskForbiddenError):
|
with pytest.raises(TaskForbiddenError):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue