Skip to content

Commit 47f6716

Browse files
authored
chore: tests fix/update (#60)
* 1. Scope mismatch bug — Session-scoped credentials + function-scoped user fixtures caused silent test skips 2. DRY violation — Duplicate _cleanup fixture now centralized 3. Deprecated asyncio pattern — Uses modern get_running_loop() API * asyncio.get_event_loop -> get_running_loop * tests update: DI system instead of global circus * DI change: using from_context instead of separate container * single owner pattern
1 parent a22ffae commit 47f6716

File tree

74 files changed

+1406
-1723
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+1406
-1723
lines changed

.github/workflows/backend-ci.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ jobs:
9090
docker compose -f docker-compose.ci.yaml up -d --wait --wait-timeout 120
9191
docker compose -f docker-compose.ci.yaml ps
9292
93+
- name: Create Kafka topics
94+
timeout-minutes: 2
95+
env:
96+
KAFKA_BOOTSTRAP_SERVERS: localhost:9092
97+
KAFKA_TOPIC_PREFIX: "ci.${{ github.run_id }}."
98+
run: |
99+
cd backend
100+
uv run python -m scripts.create_topics
101+
93102
- name: Run integration tests
94103
timeout-minutes: 10
95104
env:
@@ -99,6 +108,7 @@ jobs:
99108
MONGODB_PORT: 27017
100109
MONGODB_URL: mongodb://root:[email protected]:27017/?authSource=admin
101110
KAFKA_BOOTSTRAP_SERVERS: localhost:9092
111+
KAFKA_TOPIC_PREFIX: "ci.${{ github.run_id }}."
102112
SCHEMA_REGISTRY_URL: http://localhost:8081
103113
REDIS_HOST: localhost
104114
REDIS_PORT: 6379
@@ -174,13 +184,23 @@ jobs:
174184
timeout 90 bash -c 'until sudo k3s kubectl cluster-info; do sleep 5; done'
175185
kubectl create namespace integr8scode --dry-run=client -o yaml | kubectl apply -f -
176186
187+
- name: Create Kafka topics
188+
timeout-minutes: 2
189+
env:
190+
KAFKA_BOOTSTRAP_SERVERS: localhost:9092
191+
KAFKA_TOPIC_PREFIX: "ci.${{ github.run_id }}."
192+
run: |
193+
cd backend
194+
uv run python -m scripts.create_topics
195+
177196
- name: Run E2E tests
178197
timeout-minutes: 10
179198
env:
180199
MONGO_ROOT_USER: root
181200
MONGO_ROOT_PASSWORD: rootpassword
182201
MONGODB_URL: mongodb://root:[email protected]:27017/?authSource=admin
183202
KAFKA_BOOTSTRAP_SERVERS: localhost:9092
203+
KAFKA_TOPIC_PREFIX: "ci.${{ github.run_id }}."
184204
SCHEMA_REGISTRY_URL: http://localhost:8081
185205
REDIS_HOST: localhost
186206
REDIS_PORT: 6379

backend/.env.test

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ REDIS_DECODE_RESPONSES=true
2222

2323
# Kafka - use localhost for tests
2424
KAFKA_BOOTSTRAP_SERVERS=localhost:9092
25+
KAFKA_TOPIC_PREFIX=test.
2526
SCHEMA_REGISTRY_URL=http://localhost:8081
2627

2728
# Security
@@ -31,9 +32,12 @@ CORS_ALLOWED_ORIGINS=["http://localhost:3000","https://localhost:3000"]
3132
# Features
3233
RATE_LIMIT_ENABLED=true
3334
ENABLE_TRACING=false
34-
OTEL_SDK_DISABLED=true
35+
36+
# OpenTelemetry - explicitly disabled for tests (no endpoint = NoOp meter)
37+
OTEL_EXPORTER_OTLP_ENDPOINT=
3538
OTEL_METRICS_EXPORTER=none
3639
OTEL_TRACES_EXPORTER=none
40+
OTEL_LOGS_EXPORTER=none
3741

3842
# Development
3943
DEVELOPMENT_MODE=false

backend/app/core/container.py

Lines changed: 129 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,174 @@
55
AdminServicesProvider,
66
AuthProvider,
77
BusinessServicesProvider,
8-
ConnectionProvider,
8+
CoordinatorProvider,
99
CoreServicesProvider,
1010
DatabaseProvider,
11+
DLQProcessorProvider,
1112
EventProvider,
13+
EventReplayProvider,
14+
K8sWorkerProvider,
15+
KafkaServicesProvider,
16+
KubernetesProvider,
1217
LoggingProvider,
1318
MessagingProvider,
19+
MetricsProvider,
20+
PodMonitorProvider,
1421
RedisProvider,
15-
ResultProcessorProvider,
22+
RepositoryProvider,
23+
SagaOrchestratorProvider,
1624
SettingsProvider,
25+
SSEProvider,
1726
UserServicesProvider,
1827
)
28+
from app.settings import Settings
1929

2030

21-
def create_app_container() -> AsyncContainer:
31+
def create_app_container(settings: Settings) -> AsyncContainer:
2232
"""
2333
Create the application DI container.
34+
35+
Args:
36+
settings: Application settings (injected via from_context).
2437
"""
2538
return make_async_container(
2639
SettingsProvider(),
2740
LoggingProvider(),
2841
DatabaseProvider(),
2942
RedisProvider(),
3043
CoreServicesProvider(),
44+
MetricsProvider(),
45+
RepositoryProvider(),
3146
MessagingProvider(),
3247
EventProvider(),
33-
ConnectionProvider(),
48+
KafkaServicesProvider(),
49+
SSEProvider(),
3450
AuthProvider(),
3551
UserServicesProvider(),
3652
AdminServicesProvider(),
3753
BusinessServicesProvider(),
3854
FastapiProvider(),
55+
context={Settings: settings},
3956
)
4057

4158

42-
def create_result_processor_container() -> AsyncContainer:
59+
def create_result_processor_container(settings: Settings) -> AsyncContainer:
4360
"""
4461
Create a minimal DI container for the ResultProcessor worker.
45-
Includes only settings, database, event/kafka, and required repositories.
62+
63+
Args:
64+
settings: Application settings (injected via from_context).
4665
"""
66+
return make_async_container(
67+
SettingsProvider(),
68+
LoggingProvider(),
69+
DatabaseProvider(),
70+
RedisProvider(),
71+
CoreServicesProvider(),
72+
MetricsProvider(),
73+
RepositoryProvider(),
74+
EventProvider(),
75+
MessagingProvider(),
76+
context={Settings: settings},
77+
)
78+
79+
80+
def create_coordinator_container(settings: Settings) -> AsyncContainer:
81+
"""Create DI container for the ExecutionCoordinator worker."""
82+
return make_async_container(
83+
SettingsProvider(),
84+
LoggingProvider(),
85+
DatabaseProvider(),
86+
RedisProvider(),
87+
CoreServicesProvider(),
88+
MetricsProvider(),
89+
RepositoryProvider(),
90+
MessagingProvider(),
91+
EventProvider(),
92+
CoordinatorProvider(),
93+
context={Settings: settings},
94+
)
95+
96+
97+
def create_k8s_worker_container(settings: Settings) -> AsyncContainer:
98+
"""Create DI container for the KubernetesWorker."""
99+
return make_async_container(
100+
SettingsProvider(),
101+
LoggingProvider(),
102+
DatabaseProvider(),
103+
RedisProvider(),
104+
CoreServicesProvider(),
105+
MetricsProvider(),
106+
RepositoryProvider(),
107+
MessagingProvider(),
108+
EventProvider(),
109+
KubernetesProvider(),
110+
K8sWorkerProvider(),
111+
context={Settings: settings},
112+
)
113+
114+
115+
def create_pod_monitor_container(settings: Settings) -> AsyncContainer:
116+
"""Create DI container for the PodMonitor worker."""
47117
return make_async_container(
48118
SettingsProvider(),
49119
LoggingProvider(),
50120
DatabaseProvider(),
51121
CoreServicesProvider(),
52-
ConnectionProvider(),
122+
MetricsProvider(),
123+
RepositoryProvider(),
124+
MessagingProvider(),
125+
EventProvider(),
126+
KafkaServicesProvider(),
127+
KubernetesProvider(),
128+
PodMonitorProvider(),
129+
context={Settings: settings},
130+
)
131+
132+
133+
def create_saga_orchestrator_container(settings: Settings) -> AsyncContainer:
134+
"""Create DI container for the SagaOrchestrator worker."""
135+
return make_async_container(
136+
SettingsProvider(),
137+
LoggingProvider(),
138+
DatabaseProvider(),
53139
RedisProvider(),
140+
CoreServicesProvider(),
141+
MetricsProvider(),
142+
RepositoryProvider(),
143+
MessagingProvider(),
54144
EventProvider(),
145+
SagaOrchestratorProvider(),
146+
context={Settings: settings},
147+
)
148+
149+
150+
def create_event_replay_container(settings: Settings) -> AsyncContainer:
151+
"""Create DI container for the EventReplay worker."""
152+
return make_async_container(
153+
SettingsProvider(),
154+
LoggingProvider(),
155+
DatabaseProvider(),
156+
CoreServicesProvider(),
157+
MetricsProvider(),
158+
RepositoryProvider(),
55159
MessagingProvider(),
56-
ResultProcessorProvider(),
160+
EventProvider(),
161+
EventReplayProvider(),
162+
context={Settings: settings},
163+
)
164+
165+
166+
def create_dlq_processor_container(settings: Settings) -> AsyncContainer:
167+
"""Create DI container for the DLQ processor worker."""
168+
return make_async_container(
169+
SettingsProvider(),
170+
LoggingProvider(),
171+
DatabaseProvider(),
172+
CoreServicesProvider(),
173+
MetricsProvider(),
174+
RepositoryProvider(),
175+
EventProvider(),
176+
DLQProcessorProvider(),
177+
context={Settings: settings},
57178
)

backend/app/core/lifecycle.py

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,62 @@
11
from __future__ import annotations
22

33
from types import TracebackType
4-
from typing import Optional, Self, Type
4+
from typing import Self
55

66

77
class LifecycleEnabled:
8-
async def start(self) -> None: # pragma: no cover
9-
raise NotImplementedError
8+
"""Base class for services with async lifecycle management.
109
11-
async def stop(self) -> None: # pragma: no cover
12-
raise NotImplementedError
10+
Usage:
11+
async with MyService() as service:
12+
# service is running
13+
# service is stopped
14+
15+
Subclasses override _on_start() and _on_stop() for their logic.
16+
Base class handles idempotency and context manager protocol.
17+
18+
For internal component cleanup, use aclose() which follows Python's
19+
standard async cleanup pattern (like aiofiles, aiohttp).
20+
"""
21+
22+
def __init__(self) -> None:
23+
self._lifecycle_started: bool = False
24+
25+
async def _on_start(self) -> None:
26+
"""Override with startup logic. Called once on enter."""
27+
pass
28+
29+
async def _on_stop(self) -> None:
30+
"""Override with cleanup logic. Called once on exit."""
31+
pass
32+
33+
async def aclose(self) -> None:
34+
"""Close the service. For internal component cleanup.
35+
36+
Mirrors Python's standard aclose() pattern (like aiofiles, aiohttp).
37+
Idempotent - safe to call multiple times.
38+
"""
39+
if not self._lifecycle_started:
40+
return
41+
self._lifecycle_started = False
42+
await self._on_stop()
43+
44+
@property
45+
def is_running(self) -> bool:
46+
"""Check if service is currently running."""
47+
return self._lifecycle_started
1348

1449
async def __aenter__(self) -> Self:
15-
await self.start()
50+
if self._lifecycle_started:
51+
return self # Already started, idempotent
52+
await self._on_start()
53+
self._lifecycle_started = True
1654
return self
1755

1856
async def __aexit__(
1957
self,
20-
exc_type: Optional[Type[BaseException]],
21-
exc: Optional[BaseException],
22-
tb: Optional[TracebackType],
58+
exc_type: type[BaseException] | None,
59+
exc: BaseException | None,
60+
tb: TracebackType | None,
2361
) -> None:
24-
await self.stop()
62+
await self.aclose()

0 commit comments

Comments
 (0)