Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
728743c
fixes
HardMax71 Jan 8, 2026
e1fcdd9
fixes
HardMax71 Jan 8, 2026
9c2b6e8
fixes
HardMax71 Jan 8, 2026
884fb04
fixes
HardMax71 Jan 8, 2026
118bd6d
fixes
HardMax71 Jan 9, 2026
277cabf
fixes
HardMax71 Jan 9, 2026
6027ac4
fixes
HardMax71 Jan 9, 2026
7cc31fe
fixes (removed duplicate logins)
HardMax71 Jan 9, 2026
4abcb71
flaky test fix
HardMax71 Jan 9, 2026
aa3c8ac
xdist-group
HardMax71 Jan 9, 2026
5489e39
pyproject fix
HardMax71 Jan 9, 2026
93a79e6
durations=0 for tests: checking which ones take most time
HardMax71 Jan 9, 2026
bc944e1
optimizations of sse routes tests
HardMax71 Jan 9, 2026
48aa71f
optimizations of sse routes tests
HardMax71 Jan 9, 2026
824d686
security service added to DI, simplified settings passing
HardMax71 Jan 9, 2026
e88e606
more DI for settings obj, also removed random bootstrap_servers being…
HardMax71 Jan 9, 2026
72a7733
mypy
HardMax71 Jan 9, 2026
2cb4d4d
removed env block from toml file, deps updated accordingly
HardMax71 Jan 9, 2026
9a22a4c
Otel transient error fix
HardMax71 Jan 9, 2026
b3cdef8
Otel transient error fix + DI for metrics
HardMax71 Jan 9, 2026
4d78cc1
other fixes
HardMax71 Jan 9, 2026
57fd0b1
other fixes
HardMax71 Jan 9, 2026
8120957
mypy fixes part 1
HardMax71 Jan 9, 2026
a3907bb
mypy fixes part 2
HardMax71 Jan 9, 2026
47d1215
mypy fixes part 3
HardMax71 Jan 9, 2026
8cde784
mypy fixes part 5; also added csrf middleware
HardMax71 Jan 9, 2026
5796a27
less fixtures, passing directly xx_user fixture as logged in user of …
HardMax71 Jan 9, 2026
726e2f9
settings fixes (lifespan now reads from DI)
HardMax71 Jan 9, 2026
f1109d5
new sse event (subscribed), updated tests, dlq retry msgs moved to ma…
HardMax71 Jan 10, 2026
4452603
fixes
HardMax71 Jan 10, 2026
a202287
fixes
HardMax71 Jan 10, 2026
c6837fc
fixes
HardMax71 Jan 10, 2026
e67ef8a
fixes
HardMax71 Jan 10, 2026
1016bf7
single source of truth regarding loading Settings - DI (not get_setti…
HardMax71 Jan 10, 2026
d375e5e
fixes + parallel execution
HardMax71 Jan 10, 2026
2f1a020
non-flaky tests
HardMax71 Jan 10, 2026
225c737
fixes
HardMax71 Jan 10, 2026
a5a1f99
fixes
HardMax71 Jan 10, 2026
9ff0f12
fixes
HardMax71 Jan 10, 2026
2f0ee35
fixes
HardMax71 Jan 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 3 additions & 39 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
run: |
cd backend
uv run pytest tests/unit -v -rs \
--durations=0 \
--cov=app \
--cov-report=xml --cov-report=term

Expand Down Expand Up @@ -90,32 +91,12 @@ jobs:
docker compose -f docker-compose.ci.yaml up -d --wait --wait-timeout 120
docker compose -f docker-compose.ci.yaml ps

- name: Create Kafka topics
timeout-minutes: 2
env:
KAFKA_BOOTSTRAP_SERVERS: localhost:9092
KAFKA_TOPIC_PREFIX: "ci.${{ github.run_id }}."
run: |
cd backend
uv run python -m scripts.create_topics

- name: Run integration tests
timeout-minutes: 10
env:
MONGO_ROOT_USER: root
MONGO_ROOT_PASSWORD: rootpassword
MONGODB_HOST: 127.0.0.1
MONGODB_PORT: 27017
MONGODB_URL: mongodb://root:[email protected]:27017/?authSource=admin
KAFKA_BOOTSTRAP_SERVERS: localhost:9092
KAFKA_TOPIC_PREFIX: "ci.${{ github.run_id }}."
SCHEMA_REGISTRY_URL: http://localhost:8081
REDIS_HOST: localhost
REDIS_PORT: 6379
SCHEMA_SUBJECT_PREFIX: "ci.${{ github.run_id }}."
run: |
cd backend
uv run pytest tests/integration -v -rs \
--durations=0 \
--cov=app \
--cov-report=xml --cov-report=term

Expand Down Expand Up @@ -184,32 +165,15 @@ jobs:
timeout 90 bash -c 'until sudo k3s kubectl cluster-info; do sleep 5; done'
kubectl create namespace integr8scode --dry-run=client -o yaml | kubectl apply -f -

- name: Create Kafka topics
timeout-minutes: 2
env:
KAFKA_BOOTSTRAP_SERVERS: localhost:9092
KAFKA_TOPIC_PREFIX: "ci.${{ github.run_id }}."
run: |
cd backend
uv run python -m scripts.create_topics

- name: Run E2E tests
timeout-minutes: 10
env:
MONGO_ROOT_USER: root
MONGO_ROOT_PASSWORD: rootpassword
MONGODB_URL: mongodb://root:[email protected]:27017/?authSource=admin
KAFKA_BOOTSTRAP_SERVERS: localhost:9092
KAFKA_TOPIC_PREFIX: "ci.${{ github.run_id }}."
SCHEMA_REGISTRY_URL: http://localhost:8081
REDIS_HOST: localhost
REDIS_PORT: 6379
SCHEMA_SUBJECT_PREFIX: "ci.${{ github.run_id }}."
KUBECONFIG: /home/runner/.kube/config
K8S_NAMESPACE: integr8scode
run: |
cd backend
uv run pytest tests/e2e -v -rs \
--durations=0 \
--cov=app \
--cov-report=xml --cov-report=term

Expand Down
3 changes: 3 additions & 0 deletions backend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,6 @@ WEB_BACKLOG=2048
# When running uvicorn locally (outside Docker), bind to IPv4 loopback to avoid
# IPv6-only localhost resolution on some Linux distros.
SERVER_HOST=127.0.0.1

# Security
BCRYPT_ROUNDS=12
10 changes: 4 additions & 6 deletions backend/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ PROJECT_NAME=integr8scode
DATABASE_NAME=integr8scode_test
API_V1_STR=/api/v1
SECRET_KEY=test-secret-key-for-testing-only-32chars!!
ENVIRONMENT=testing
TESTING=true

# MongoDB - use localhost for tests
Expand All @@ -23,22 +22,21 @@ REDIS_DECODE_RESPONSES=true
# Kafka - use localhost for tests
KAFKA_BOOTSTRAP_SERVERS=localhost:9092
KAFKA_TOPIC_PREFIX=test.
SCHEMA_SUBJECT_PREFIX=test.
SCHEMA_REGISTRY_URL=http://localhost:8081

# Security
SECURE_COOKIES=true
CORS_ALLOWED_ORIGINS=["http://localhost:3000","https://localhost:3000"]
BCRYPT_ROUNDS=4

# Features
RATE_LIMIT_ENABLED=true
ENABLE_TRACING=false

# OpenTelemetry - explicitly disabled for tests (no endpoint = NoOp meter)
# OpenTelemetry - explicitly disabled for tests
OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_METRICS_EXPORTER=none
OTEL_TRACES_EXPORTER=none
OTEL_LOGS_EXPORTER=none

# Development
DEVELOPMENT_MODE=false
LOG_LEVEL=INFO
ENVIRONMENT=test
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ done
echo "Starting application..."
[ -f /app/kubeconfig.yaml ] && export KUBECONFIG=/app/kubeconfig.yaml

exec gunicorn app.main:app \
exec gunicorn 'app.main:create_app()' \
-k uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:443 \
--workers ${WEB_CONCURRENCY:-4} \
Expand Down
9 changes: 5 additions & 4 deletions backend/app/api/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from fastapi.security import OAuth2PasswordRequestForm
from pymongo.errors import DuplicateKeyError

from app.core.security import security_service
from app.core.security import SecurityService
from app.core.utils import get_client_ip
from app.db.repositories import UserRepository
from app.domain.user import DomainUserCreate
Expand All @@ -19,7 +19,7 @@
UserResponse,
)
from app.services.auth_service import AuthService
from app.settings import get_settings
from app.settings import Settings

router = APIRouter(prefix="/auth", tags=["authentication"], route_class=DishkaRoute)

Expand All @@ -29,6 +29,8 @@ async def login(
request: Request,
response: Response,
user_repo: FromDishka[UserRepository],
security_service: FromDishka[SecurityService],
settings: FromDishka[Settings],
logger: FromDishka[logging.Logger],
form_data: OAuth2PasswordRequestForm = Depends(),
) -> LoginResponse:
Expand Down Expand Up @@ -74,8 +76,6 @@ async def login(
headers={"WWW-Authenticate": "Bearer"},
)

settings = get_settings()

logger.info(
"Login successful",
extra={
Expand Down Expand Up @@ -127,6 +127,7 @@ async def register(
request: Request,
user: UserCreate,
user_repo: FromDishka[UserRepository],
security_service: FromDishka[SecurityService],
logger: FromDishka[logging.Logger],
) -> UserResponse:
logger.info(
Expand Down
4 changes: 2 additions & 2 deletions backend/app/api/routes/dlq.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ async def get_dlq_message(event_id: str, repository: FromDishka[DLQRepository])

@router.post("/retry", response_model=DLQBatchRetryResponse)
async def retry_dlq_messages(
retry_request: ManualRetryRequest, repository: FromDishka[DLQRepository], dlq_manager: FromDishka[DLQManager]
retry_request: ManualRetryRequest, dlq_manager: FromDishka[DLQManager]
) -> DLQBatchRetryResponse:
result = await repository.retry_messages_batch(retry_request.event_ids, dlq_manager)
result = await dlq_manager.retry_messages_batch(retry_request.event_ids)
return DLQBatchRetryResponse(
total=result.total,
successful=result.successful,
Expand Down
6 changes: 3 additions & 3 deletions backend/app/api/routes/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from app.schemas_pydantic.user import UserResponse
from app.services.event_service import EventService
from app.services.kafka_event_service import KafkaEventService
from app.settings import get_settings
from app.settings import Settings

router = APIRouter(prefix="/events", tags=["events"], route_class=DishkaRoute)

Expand Down Expand Up @@ -229,8 +229,8 @@ async def publish_custom_event(
event_request: PublishEventRequest,
request: Request,
event_service: FromDishka[KafkaEventService],
settings: FromDishka[Settings],
) -> PublishEventResponse:
settings = get_settings()
base_meta = EventMetadata(
service_name=settings.SERVICE_NAME,
service_version=settings.SERVICE_VERSION,
Expand Down Expand Up @@ -311,6 +311,7 @@ async def replay_aggregate_events(
admin: Annotated[UserResponse, Depends(admin_user)],
event_service: FromDishka[EventService],
kafka_event_service: FromDishka[KafkaEventService],
settings: FromDishka[Settings],
logger: FromDishka[logging.Logger],
target_service: str | None = Query(None, description="Service to replay events to"),
dry_run: bool = Query(True, description="If true, only show what would be replayed"),
Expand Down Expand Up @@ -339,7 +340,6 @@ async def replay_aggregate_events(
await asyncio.sleep(0.1)

try:
settings = get_settings()
meta = EventMetadata(
service_name=settings.SERVICE_NAME,
service_version=settings.SERVICE_VERSION,
Expand Down
4 changes: 2 additions & 2 deletions backend/app/api/routes/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from app.services.execution_service import ExecutionService
from app.services.idempotency import IdempotencyManager
from app.services.kafka_event_service import KafkaEventService
from app.settings import get_settings
from app.settings import Settings

router = APIRouter(route_class=DishkaRoute)

Expand Down Expand Up @@ -162,6 +162,7 @@ async def cancel_execution(
current_user: Annotated[UserResponse, Depends(current_user)],
cancel_request: CancelExecutionRequest,
event_service: FromDishka[KafkaEventService],
settings: FromDishka[Settings],
) -> CancelResponse:
# Handle terminal states
terminal_states = [ExecutionStatus.COMPLETED, ExecutionStatus.FAILED, ExecutionStatus.TIMEOUT]
Expand All @@ -178,7 +179,6 @@ async def cancel_execution(
event_id="-1", # exact event_id unknown
)

settings = get_settings()
payload = {
"execution_id": execution.execution_id,
"status": str(ExecutionStatus.CANCELLED),
Expand Down
9 changes: 3 additions & 6 deletions backend/app/core/adaptive_sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
import threading
import time
from collections import deque
from typing import Any, Sequence, Tuple
from typing import Sequence, Tuple

from opentelemetry.context import Context
from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult
from opentelemetry.trace import Link, SpanKind, TraceState, get_current_span
from opentelemetry.util.types import Attributes

from app.settings import get_settings
from app.settings import Settings


class AdaptiveSampler(Sampler):
Expand Down Expand Up @@ -239,11 +239,8 @@ def shutdown(self) -> None:
self._adjustment_thread.join(timeout=5.0)


def create_adaptive_sampler(settings: Any | None = None) -> AdaptiveSampler:
def create_adaptive_sampler(settings: Settings) -> AdaptiveSampler:
"""Create adaptive sampler with settings"""
if settings is None:
settings = get_settings()

return AdaptiveSampler(
base_rate=settings.TRACING_SAMPLING_RATE,
min_rate=max(0.001, settings.TRACING_SAMPLING_RATE / 100), # 1/100th of base
Expand Down
1 change: 1 addition & 0 deletions backend/app/core/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def create_app_container(settings: Settings) -> AsyncContainer:
RepositoryProvider(),
MessagingProvider(),
EventProvider(),
SagaOrchestratorProvider(),
KafkaServicesProvider(),
SSEProvider(),
AuthProvider(),
Expand Down
46 changes: 26 additions & 20 deletions backend/app/core/dishka_lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from app.events.event_store_consumer import EventStoreConsumer
from app.events.schema.schema_registry import SchemaRegistryManager, initialize_event_schemas
from app.services.sse.kafka_redis_bridge import SSEKafkaRedisBridge
from app.settings import get_settings
from app.settings import Settings


@asynccontextmanager
Expand All @@ -27,10 +27,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
- No manual service management
- Dishka handles all lifecycle automatically
"""
settings = get_settings()

# Get logger from DI container
# Get settings and logger from DI container (uses test settings in tests)
container: AsyncContainer = app.state.dishka_container
settings = await container.get(Settings)
logger = await container.get(logging.Logger)

logger.info(
Expand All @@ -44,25 +43,32 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
# Metrics setup moved to app creation to allow middleware registration
logger.info("Lifespan start: tracing and services initialization")

# Initialize tracing
instrumentation_report = init_tracing(
service_name=settings.TRACING_SERVICE_NAME,
logger=logger,
service_version=settings.TRACING_SERVICE_VERSION,
sampling_rate=settings.TRACING_SAMPLING_RATE,
enable_console_exporter=settings.TESTING,
adaptive_sampling=settings.TRACING_ADAPTIVE_SAMPLING,
)

if instrumentation_report.has_failures():
logger.warning(
"Some instrumentation libraries failed to initialize",
extra={"instrumentation_summary": instrumentation_report.get_summary()},
# Initialize tracing only when enabled (avoid exporter retries in tests)
if settings.ENABLE_TRACING and not settings.TESTING:
instrumentation_report = init_tracing(
service_name=settings.TRACING_SERVICE_NAME,
settings=settings,
logger=logger,
service_version=settings.TRACING_SERVICE_VERSION,
sampling_rate=settings.TRACING_SAMPLING_RATE,
enable_console_exporter=settings.TESTING,
adaptive_sampling=settings.TRACING_ADAPTIVE_SAMPLING,
)

if instrumentation_report.has_failures():
logger.warning(
"Some instrumentation libraries failed to initialize",
extra={"instrumentation_summary": instrumentation_report.get_summary()},
)
else:
logger.info(
"Distributed tracing initialized successfully",
extra={"instrumentation_summary": instrumentation_report.get_summary()},
)
else:
logger.info(
"Distributed tracing initialized successfully",
extra={"instrumentation_summary": instrumentation_report.get_summary()},
"Distributed tracing disabled",
extra={"testing": settings.TESTING, "enable_tracing": settings.ENABLE_TRACING},
)

# Initialize schema registry once at startup
Expand Down
5 changes: 5 additions & 0 deletions backend/app/core/k8s_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@

from kubernetes import client as k8s_client
from kubernetes import config as k8s_config
from kubernetes import watch as k8s_watch


@dataclass(frozen=True)
class K8sClients:
"""Kubernetes API clients bundle for dependency injection."""

api_client: k8s_client.ApiClient
v1: k8s_client.CoreV1Api
apps_v1: k8s_client.AppsV1Api
networking_v1: k8s_client.NetworkingV1Api
watch: k8s_watch.Watch


def create_k8s_clients(
Expand All @@ -33,6 +37,7 @@ def create_k8s_clients(
v1=k8s_client.CoreV1Api(api_client),
apps_v1=k8s_client.AppsV1Api(api_client),
networking_v1=k8s_client.NetworkingV1Api(api_client),
watch=k8s_watch.Watch(),
)


Expand Down
Loading