Skip to content

Commit a843364

Browse files
add integration tests for bot commands
1 parent e117948 commit a843364

17 files changed

Lines changed: 284 additions & 106 deletions

File tree

app/application/repository/interfaces.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Record repository interface."""
22

33
from abc import ABC, abstractmethod
4-
from typing import List
4+
from typing import List, Self
55

66
from app.domain.entities.sample_record import SampleRecord
77

@@ -23,6 +23,7 @@ class ISampleRecordUnitOfWork(IUnitOfWork, ABC):
2323

2424
@abstractmethod
2525
def get_sample_record_repository(self) -> "ISampleRecordRepository":
26+
"""Return an initialized ISampleRecordRepository object implementation."""
2627
pass
2728

2829

@@ -81,7 +82,3 @@ async def get_all(self) -> List[SampleRecord]:
8182
"""Get all records from the database"""
8283
pass
8384

84-
@abstractmethod
85-
async def commit(self) -> None:
86-
"""Commit changes to the database"""
87-
pass

app/application/use_cases/record_use_cases.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313

1414
class SampleRecordUseCases(ISampleRecordUseCases):
15-
"""Implementation of record use cases."""
15+
"""Implementation of samplr record use cases."""
1616

1717
def __init__(self, record_repo: ISampleRecordRepository):
1818
self._repo = record_repo

app/decorators/mapper/context.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44

55
class ExceptionContext:
6+
"""Class to store exception rising context."""
67
SENSITIVE_KEYS: frozenset[str] = frozenset(
78
("password", "token", "key", "secret", "auth", "credential", "passwd")
89
)
@@ -21,6 +22,8 @@ def __init__(
2122

2223
@cached_property
2324
def formatted_context(self) -> str:
25+
"""Format exception context for logging.
26+
"""
2427
error_context = [
2528
f"Error in function '{self.func.__module__}.{self.func.__qualname__}'"
2629
]
@@ -42,6 +45,10 @@ def _sanitised_value(
4245
value: Any,
4346
key: str | None = None,
4447
) -> str:
48+
"""Exclude sensitive data from logging
49+
50+
TODO: add deeper sanitation for nested structures
51+
"""
4552
if key is not None and key.lower() in self.SENSITIVE_KEYS:
4653
return "****HIDDEN****"
4754

app/decorators/mapper/exception_mapper.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414

1515

1616
class ExceptionMapper:
17-
"""Exception-mapping decorator with bounded LRU caching and dynamic MRO lookup."""
17+
"""Exception-mapping decorator with bounded LRU caching and dynamic MRO lookup.
18+
19+
The main decorator purpose is map exception between application layers and enrich exceptions by context.
20+
"""
1821

1922
def __init__(
2023
self,
@@ -38,6 +41,7 @@ def _get_exceptions_flat_map(
3841
self,
3942
exception_map: dict[ExceptionOrTupleOfExceptions, ExceptionFactory],
4043
) -> dict[Type[Exception], ExceptionFactory]:
44+
"""Do a flat map from given exception map."""
4145
flat_map: dict[Type[Exception], ExceptionFactory] = {}
4246
for exception_class, factory in exception_map.items():
4347
if isinstance(exception_class, tuple):
@@ -96,7 +100,7 @@ def _get_exception_factory(
96100
self._lru_cache[exc_type] = target_exception_factory
97101
return target_exception_factory
98102

99-
# exception is not presented in base mapping, but Exception in base mapping
103+
# exception is not presented in base mapping, but catchall presented in mapping dict
100104
if self.exception_catchall_factory:
101105
self._lru_cache[exc_type] = self.exception_catchall_factory
102106
return self.exception_catchall_factory

app/infrastructure/repositories/sample_record.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
RecordUpdateError,
1414
RecordAlreadyExistsError,
1515
ForeignKeyError,
16-
ValidationError, BaseRepositoryError,
16+
ValidationError,
17+
BaseRepositoryError,
1718
)
1819
from app.application.repository.interfaces import ISampleRecordRepository
1920
from app.decorators.mapper.exception_mapper import (
@@ -58,15 +59,6 @@ def __init__(self, session: AsyncSession):
5859
"""
5960
self._session = session
6061

61-
@ExceptionMapper(
62-
{
63-
Exception: EnrichedExceptionFactory(BaseRepositoryError),
64-
},
65-
is_bound_method=True,
66-
)
67-
async def commit(self) -> None:
68-
await self._session.commit()
69-
7062
@ExceptionMapper(
7163
{
7264
IntegrityError: IntegrityErrorFactory(RecordCreateError),

app/main.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ async def shutdown(
3333
await container.callback_task_manager().shutdown()
3434

3535
await container.redis_client().aclose()
36-
await container.shutdown_resources()
3736
await get_engine().dispose()
3837

3938

app/presentation/bot/command_handlers/sample_record.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
RecordAlreadyExistsError,
88
RecordDoesNotExistError,
99
)
10-
from app.application.repository.interfaces import ISampleRecordUnitOfWork, \
11-
ISampleRecordRepository
10+
from app.application.repository.interfaces import (
11+
ISampleRecordUnitOfWork,
12+
ISampleRecordRepository,
13+
)
1214
from app.application.use_cases.interfaces import ISampleRecordUseCases
1315
from app.presentation.bot.command_handlers.base_handler import BaseCommandHandler
1416
from app.presentation.bot.error_handlers.exceptions_chain_executor import (
@@ -54,7 +56,7 @@ def __init__(
5456
bot: Bot,
5557
message: IncomingMessage,
5658
unit_of_work: ISampleRecordUnitOfWork,
57-
use_case_factory: Callable[[ISampleRecordRepository], ISampleRecordUseCases]
59+
use_case_factory: Callable[[ISampleRecordRepository], ISampleRecordUseCases],
5860
):
5961
self._use_cases = use_case_factory
6062
self.unit_of_work = unit_of_work
@@ -97,17 +99,23 @@ def __init__(
9799
self,
98100
bot: Bot,
99101
message: IncomingMessage,
100-
use_cases: ISampleRecordUseCases,
102+
unit_of_work: ISampleRecordUnitOfWork,
103+
use_case_factory: Callable[[ISampleRecordRepository], ISampleRecordUseCases],
101104
):
102-
self._use_cases = use_cases
105+
self._use_cases = use_case_factory
106+
self.unit_of_work = unit_of_work
103107

104108
super().__init__(bot, message, self.exception_handler_chain_executor)
105109

106110
async def handle_logic(
107111
self,
108112
request_parameter: SampleRecordDeleteRequestSchema, # type: ignore
109113
) -> None:
110-
await self._use_cases.delete_record(request_parameter.id)
114+
async with self.unit_of_work as uof:
115+
await self._use_cases(uof.get_sample_record_repository()).delete_record(
116+
request_parameter.id
117+
)
118+
111119
await self._bot.answer_message(
112120
SAMPLE_RECORD_DELETED_ANSWER.format(
113121
id=request_parameter.id,

app/presentation/bot/commands/sample_record.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
BotSampleRecordCommandContainer,
1212
ApplicationStartupContainer,
1313
)
14-
from app.infrastructure.db.sqlalchemy import provide_session
14+
1515
from app.infrastructure.repositories.unit_of_work import WriteSampleRecordUnitOfWork
1616
from app.presentation.bot.commands.command_listing import SampleRecordCommands
1717
from app.presentation.bot.command_handlers.sample_record import (
@@ -23,16 +23,16 @@
2323

2424

2525
@collector.command(**SampleRecordCommands.CREATE_RECORD.command_data())
26-
# @provide_session
2726
@inject
2827
async def create_sample_record(
2928
message: IncomingMessage,
3029
bot: Bot,
31-
# session: AsyncSession,
32-
unit_of_work: WriteSampleRecordUnitOfWork=Provide[BotSampleRecordCommandContainer.rw_unit_of_work],
33-
record_use_cases_factory: Callable[[ISampleRecordRepository], ISampleRecordUseCases] = Provider[
34-
BotSampleRecordCommandContainer.record_use_cases_factory
30+
unit_of_work: WriteSampleRecordUnitOfWork = Provide[
31+
BotSampleRecordCommandContainer.rw_unit_of_work
3532
],
33+
record_use_cases_factory: Callable[
34+
[ISampleRecordRepository], ISampleRecordUseCases
35+
] = Provider[BotSampleRecordCommandContainer.record_use_cases_factory],
3636
) -> None:
3737
"""Creates a sample record in the database."""
3838
handler = CreateSampleRecordHandler(
@@ -46,19 +46,21 @@ async def create_sample_record(
4646

4747

4848
@collector.command(**SampleRecordCommands.DELETE_RECORD.command_data())
49-
# @provide_session
5049
@inject
5150
async def delete_sample_record(
5251
message: IncomingMessage,
5352
bot: Bot,
54-
session: AsyncSession,
55-
record_use_cases_factory: Factory[ISampleRecordUseCases] = Provider[
56-
BotSampleRecordCommandContainer.record_use_cases_factory
57-
],
53+
unit_of_work: WriteSampleRecordUnitOfWork = Provide[
54+
BotSampleRecordCommandContainer.rw_unit_of_work
55+
],
56+
record_use_cases_factory: Callable[
57+
[ISampleRecordRepository], ISampleRecordUseCases
58+
] = Provider[BotSampleRecordCommandContainer.record_use_cases_factory],
5859
) -> None:
5960
"""Delete a sample record in the database."""
6061
await DeleteSampleRecordHandler(
6162
bot=bot,
6263
message=message,
63-
use_cases=record_use_cases_factory(session),
64+
use_case_factory=record_use_cases_factory,
65+
unit_of_work=unit_of_work,
6466
).execute()

app/presentation/bot/middlewares/answer_error.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
async def answer_error_middleware(
2020
message: IncomingMessage, bot: Bot, call_next: IncomingMessageHandlerFunc
2121
) -> None:
22+
"""Middleware, used for catching and logging unhandled AnswerError and AnswerMessageError."""
2223
try:
2324
await call_next(message, bot)
2425
except AnswerError as exc:

tests/conftest.py

Lines changed: 2 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
import asyncio
22
from datetime import datetime
3-
from typing import Any, Callable, Dict, Optional
4-
from uuid import UUID, uuid4
3+
from typing import Any, Dict
4+
from uuid import UUID
55

66
import jwt
77
import pytest
8-
from pybotx import (
9-
BotAccount,
10-
Chat,
11-
ChatTypes,
12-
IncomingMessage,
13-
UserDevice,
14-
UserSender,
15-
)
168
from testcontainers.postgres import PostgresContainer # type: ignore
179

1810
from app.settings import settings
@@ -72,54 +64,3 @@ def authorization_header(
7264
return {"authorization": f"Bearer {token}"}
7365

7466

75-
@pytest.fixture
76-
def incoming_message_factory(
77-
bot_id: UUID,
78-
user_huid: UUID,
79-
host: str,
80-
) -> Callable[..., IncomingMessage]:
81-
def factory(
82-
*,
83-
body: str = "",
84-
ad_login: Optional[str] = None,
85-
ad_domain: Optional[str] = None,
86-
) -> IncomingMessage:
87-
return IncomingMessage(
88-
bot=BotAccount(
89-
id=bot_id,
90-
host=host,
91-
),
92-
sync_id=uuid4(),
93-
source_sync_id=None,
94-
body=body,
95-
data={},
96-
metadata={},
97-
sender=UserSender(
98-
huid=user_huid,
99-
udid=None,
100-
ad_login=ad_login,
101-
ad_domain=ad_domain,
102-
username=None,
103-
is_chat_admin=True,
104-
is_chat_creator=True,
105-
device=UserDevice(
106-
manufacturer=None,
107-
device_name=None,
108-
os=None,
109-
pushes=None,
110-
timezone=None,
111-
permissions=None,
112-
platform=None,
113-
platform_package_id=None,
114-
app_version=None,
115-
locale=None,
116-
),
117-
),
118-
chat=Chat(
119-
id=uuid4(),
120-
type=ChatTypes.PERSONAL_CHAT,
121-
),
122-
raw_command=None,
123-
)
124-
125-
return factory

0 commit comments

Comments
 (0)