Skip to content

Commit a02ea4c

Browse files
author
Max Azatian
committed
v2.1: 80% coverage, updated tests, updated readmes
1 parent 80e763f commit a02ea4c

File tree

356 files changed

+22263
-25697
lines changed

Some content is hidden

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

356 files changed

+22263
-25697
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ jobs:
124124
run: |
125125
cd backend
126126
echo "Using BACKEND_BASE_URL=$BACKEND_BASE_URL"
127-
python -m pytest tests/integration tests/unit -v --cov=app --cov-report=xml --cov-report=term
127+
python -m pytest tests/integration tests/unit -v --cov=app --cov-branch --cov-report=xml --cov-report=term --cov-report=term-missing
128128
129129
- name: Upload coverage to Codecov
130130
uses: codecov/codecov-action@v5

ARCHITECTURE_IN_DETAILS.md

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,8 @@ This document sketches the system as it actually exists in this repo, using ASCI
77

88
```plantuml
99
@startuml
10-
skinparam monochrome true
11-
skinparam shadowing false
12-
1310
rectangle "Public Internet\n(Browser SPA)" as Browser
1411
rectangle "Frontend\n(Nginx + Svelte)" as Frontend
15-
1612
node "Backend" as Backend {
1713
[FastAPI / Uvicorn\n(routers, Dishka DI, middlewares)] as FastAPI
1814
[SSE Service\n(Partitioned router + Redis bus)] as SSE
@@ -21,12 +17,11 @@ node "Backend" as Backend {
2117
cloud "Kafka" as Kafka
2218
[Schema Registry] as Schema
2319
cloud "Kubernetes API" as K8s
24-
[Prometheus] as Prom
20+
[OTel Collector] as OTel
21+
[VictoriaMetrics] as VM
2522
[Jaeger] as Jaeger
2623
}
27-
2824
rectangle "Cert Generator\n(setup-k8s.sh, TLS)" as CertGen
29-
3025
Browser --> Frontend : HTTPS 443\nSPA + static assets
3126
Frontend --> FastAPI : HTTPS /api/v1/*\nCookies/CSRF
3227
FastAPI <--> SSE : /api/v1/events/*\nJSON frames
@@ -36,10 +31,10 @@ FastAPI --> Kafka : UnifiedProducer\n(events)
3631
Kafka --> FastAPI : UnifiedConsumer\n(dispatch)
3732
Kafka -- Schema
3833
FastAPI <--> K8s : pod create/monitor\nworker + pod monitor
39-
FastAPI --> Prom : metrics (pull)
34+
FastAPI --> OTel : metrics/traces (export)
35+
OTel --> VM : remote_write (metrics)
4036
FastAPI --> Jaeger : traces (export)
4137
CertGen .. K8s : cluster setup / certs
42-
4338
@enduml
4439
```
4540

@@ -76,7 +71,7 @@ Frontend serves the SPA; the SPA calls FastAPI over HTTPS. Backend exposes REST
7671
*** /admin/users (api/routes/admin/users.py)
7772
*** /admin/events (api/routes/admin/events.py)
7873
*** /admin/settings (api/routes/admin/settings.py)
79-
*** /alertmanager (api/routes/alertmanager.py)
74+
*** /alerts (api/routes/grafana_alerts.py)
8075
** DI & Providers (Dishka)
8176
*** Container (core/container.py, core/providers.py)
8277
*** Exception handlers (core/exceptions/handlers.py)
@@ -89,7 +84,7 @@ Frontend serves the SPA; the SPA calls FastAPI over HTTPS. Backend exposes REST
8984
*** SSEService (services/sse/sse_service.py)
9085
**** SSERedisBus, PartitionedSSERouter, SSEShutdownManager, EventBuffer
9186
*** NotificationService (services/notification_service.py)
92-
**** UnifiedConsumer handlers (completed/failed/timeout), templates, throttle
87+
**** UnifiedConsumer handlers (completed/failed/timeout), SSE, throttle
9388
*** UserSettingsService (services/user_settings_service.py)
9489
**** LRU cache, USER_* events to EventStore/Kafka
9590
*** SavedScriptService (services/saved_script_service.py)
@@ -134,7 +129,7 @@ Frontend serves the SPA; the SPA calls FastAPI over HTTPS. Backend exposes REST
134129
*** Redis (rate limit, SSE bus)
135130
*** Kafka + Schema Registry
136131
*** Kubernetes API (pods)
137-
*** Prometheus (metrics)
132+
*** OTel Collector + VictoriaMetrics (metrics)
138133
*** Jaeger (traces)
139134
** Settings (app/settings.py)
140135
*** Runtimes/limits, Kafka/Redis/Mongo endpoints, SSE, rate limiting
@@ -266,7 +261,7 @@ Sagas use explicit DI (no context-based injection). Only serializable public dat
266261
v
267262
NotificationService (private)
268263
|-- UnifiedConsumer (typed handlers for completed/failed/timeout)
269-
|-- Repository: templates, notifications (Mongo)
264+
|-- Repository: notifications + subscriptions (Mongo)
270265
|-- Channels:
271266
| - IN_APP: persist + publish SSE bus (Redis)
272267
| - WEBHOOK: httpx POST
@@ -331,7 +326,7 @@ Saved scripts are simple CRUD per user. User settings are reconstructed from sna
331326
## DLQ and admin tooling
332327

333328
```
334-
Kafka DLQ topic <-> DLQ consumer/manager (retry/backoff, thresholds)
329+
Kafka DLQ topic <-> DLQ manager (retry/backoff, thresholds)
335330
/api/v1/admin/events/* -> admin repos (Mongo) for events query/delete
336331
/api/v1/admin/users/* -> users repo (Mongo) + rate limit config
337332
/api/v1/admin/settings/* -> system settings (Mongo)

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ things safe and efficient. You'll get the results back in no time.
5454
- Backend: `https://127.0.0.1:443/`
5555
- To check if it works, you can use `curl -k https://127.0.0.1/api/v1/k8s-limits`, should return JSON with current limits
5656
- Grafana: `http://127.0.0.1:3000` (login - `admin`, pw - `admin123`)
57-
- Prometheus: `http://127.0.0.1:9090/targets` (`integr8scode` must be `1/1 up`)
57+
5858

5959
You may also find out that k8s doesn't capture metrics (`CPU` and `Memory` params are `null`), it may well be that metrics server
6060
for k8s is turned off/not enabled. To enable, execute:
@@ -175,6 +175,5 @@ The platform is built on three main pillars:
175175
- **Monitoring Tools**: Using OpenTelemetry and Grafana to keep an eye on system health.
176176
- **Alerts**: Set up notifications for when things go wrong.
177177

178-
Link for accessing Prometheus is shown in `/editor` web page.
179-
178+
180179

backend/.env.test

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,39 @@
11
# Test environment configuration
2-
# This file is loaded by tests/conftest.py for integration tests
3-
4-
# MongoDB Configuration
5-
MONGODB_URL="mongodb://localhost:27017"
6-
PROJECT_NAME="integr8scode_test"
7-
8-
# Redis Configuration
9-
REDIS_URL="redis://localhost:6379/0"
10-
11-
# Authentication
12-
SECRET_KEY="test-secret-key-for-testing-only"
13-
JWT_SECRET_KEY="test-jwt-secret-key-for-testing-only"
14-
JWT_ALGORITHM="HS256"
15-
ACCESS_TOKEN_EXPIRE_MINUTES=30
16-
17-
# Rate Limiting - DISABLED for tests
18-
RATE_LIMIT_ENABLED=false
19-
RATE_LIMIT_DEFAULT_REQUESTS=1000
20-
RATE_LIMIT_DEFAULT_WINDOW=1
2+
PROJECT_NAME=integr8scode_test
3+
API_V1_STR=/api/v1
4+
SECRET_KEY=test-secret-key-for-testing-only-32chars!!
5+
ENVIRONMENT=testing
6+
TESTING=true
217

22-
# Disable tracing for tests
8+
# MongoDB - use localhost for tests
9+
MONGODB_URL=mongodb://root:rootpassword@localhost:27017/?authSource=admin
10+
MONGO_ROOT_USER=root
11+
MONGO_ROOT_PASSWORD=rootpassword
12+
13+
# Redis - use localhost for tests
14+
REDIS_HOST=localhost
15+
REDIS_PORT=6379
16+
REDIS_DB=0
17+
REDIS_PASSWORD=
18+
REDIS_SSL=false
19+
REDIS_MAX_CONNECTIONS=50
20+
REDIS_DECODE_RESPONSES=true
21+
22+
# Kafka - use localhost for tests
23+
KAFKA_BOOTSTRAP_SERVERS=localhost:9092
24+
SCHEMA_REGISTRY_URL=http://localhost:8081
25+
26+
# Security
27+
SECURE_COOKIES=true
28+
CORS_ALLOWED_ORIGINS=["http://localhost:3000","https://localhost:3000"]
29+
30+
# Features
31+
RATE_LIMIT_ENABLED=true
2332
ENABLE_TRACING=false
2433
OTEL_SDK_DISABLED=true
2534
OTEL_METRICS_EXPORTER=none
2635
OTEL_TRACES_EXPORTER=none
2736

28-
# API Settings
29-
BACKEND_BASE_URL="https://[::1]:443"
30-
BACKEND_CORS_ORIGINS=["http://localhost:3000", "http://localhost:5173"]
31-
32-
# Kafka Configuration (minimal for tests)
33-
KAFKA_BOOTSTRAP_SERVERS="localhost:9092"
34-
KAFKA_SECURITY_PROTOCOL="PLAINTEXT"
35-
36-
# Kubernetes Configuration (mocked in tests)
37-
K8S_IN_CLUSTER=false
38-
K8S_NAMESPACE="default"
39-
40-
# Test Mode
41-
TESTING=true
42-
DEBUG=false
37+
# Development
38+
DEVELOPMENT_MODE=false
39+
LOG_LEVEL=INFO

backend/Dockerfile.test

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Test runner container - lightweight, uses same network as services
2+
FROM python:3.12-slim
3+
4+
WORKDIR /app
5+
6+
# Install system dependencies
7+
RUN apt-get update && apt-get install -y \
8+
gcc \
9+
curl \
10+
&& rm -rf /var/lib/apt/lists/*
11+
12+
# Copy requirements
13+
COPY requirements.txt requirements-dev.txt ./
14+
15+
# Install Python dependencies
16+
RUN pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt
17+
18+
# Copy application code
19+
COPY . .
20+
21+
# Set Python path
22+
ENV PYTHONPATH=/app
23+
24+
# Default command runs all tests
25+
CMD ["pytest", "-v", "--tb=short"]

backend/alertmanager/alertmanager.yml

Lines changed: 0 additions & 66 deletions
This file was deleted.

backend/app/api/dependencies.py

Lines changed: 15 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,24 @@
1-
from typing import Optional
2-
31
from dishka import FromDishka
42
from dishka.integrations.fastapi import inject
5-
from fastapi import HTTPException, Request, status
6-
7-
from app.core.logging import logger
8-
from app.core.security import security_service
9-
from app.db.repositories.user_repository import UserRepository
10-
from app.domain.enums.user import UserRole
11-
from app.schemas_pydantic.user import User, UserResponse
12-
13-
14-
class AuthService:
15-
def __init__(self, user_repo: UserRepository):
16-
self.user_repo = user_repo
17-
18-
async def get_current_user(self, request: Request) -> UserResponse:
19-
try:
20-
token = request.cookies.get("access_token")
21-
if not token:
22-
raise HTTPException(
23-
status_code=status.HTTP_401_UNAUTHORIZED,
24-
detail="Not authenticated",
25-
headers={"WWW-Authenticate": "Bearer"},
26-
)
3+
from fastapi import Request
274

28-
user_in_db = await security_service.get_current_user(token, self.user_repo)
29-
30-
return UserResponse(
31-
user_id=user_in_db.user_id,
32-
username=user_in_db.username,
33-
email=user_in_db.email,
34-
role=user_in_db.role,
35-
is_superuser=user_in_db.is_superuser,
36-
created_at=user_in_db.created_at,
37-
updated_at=user_in_db.updated_at
38-
)
39-
except Exception as e:
40-
logger.error(f"Authentication failed: {e}", exc_info=True)
41-
raise HTTPException(
42-
status_code=status.HTTP_401_UNAUTHORIZED,
43-
detail="Not authenticated",
44-
headers={"WWW-Authenticate": "Bearer"},
45-
) from e
46-
47-
async def require_admin(self, request: Request) -> UserResponse:
48-
user = await self.get_current_user(request)
49-
if user.role != UserRole.ADMIN:
50-
logger.warning(
51-
f"Admin access denied for user: {user.username} (role: {user.role})"
52-
)
53-
raise HTTPException(
54-
status_code=status.HTTP_403_FORBIDDEN,
55-
detail="Admin access required"
56-
)
57-
return user
58-
59-
60-
@inject
61-
async def require_auth_guard(
62-
request: Request,
63-
auth_service: FromDishka[AuthService],
64-
) -> None:
65-
await auth_service.get_current_user(request)
5+
from app.schemas_pydantic.user import UserResponse
6+
from app.services.auth_service import AuthService
667

678

689
@inject
69-
async def require_admin_guard(
70-
request: Request,
71-
auth_service: FromDishka[AuthService],
72-
) -> None:
73-
await auth_service.require_admin(request)
10+
async def CurrentUser(
11+
request: Request,
12+
auth_service: FromDishka[AuthService]
13+
) -> UserResponse:
14+
"""Get authenticated user."""
15+
return await auth_service.get_current_user(request)
7416

7517

7618
@inject
77-
async def get_current_user_optional(
78-
request: Request,
79-
auth_service: FromDishka[AuthService],
80-
) -> Optional[User]:
81-
"""
82-
Get current user if authenticated, otherwise return None.
83-
This is used for optional authentication, like rate limiting.
84-
"""
85-
try:
86-
user_response = await auth_service.get_current_user(request)
87-
# Convert UserResponse to User for compatibility
88-
return User(
89-
user_id=user_response.user_id,
90-
username=user_response.username,
91-
email=user_response.email,
92-
role=user_response.role,
93-
is_active=True, # If they can authenticate, they're active
94-
is_superuser=user_response.is_superuser,
95-
created_at=user_response.created_at,
96-
updated_at=user_response.updated_at
97-
)
98-
except HTTPException:
99-
# User is not authenticated, return None
100-
return None
19+
async def AdminUser(
20+
request: Request,
21+
auth_service: FromDishka[AuthService]
22+
) -> UserResponse:
23+
"""Get authenticated admin user."""
24+
return await auth_service.get_admin(request)

0 commit comments

Comments
 (0)