Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 7 additions & 7 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,24 @@ jobs:
uses: actions/checkout@v6

- name: Set up uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- name: Install MkDocs
run: uv tool install mkdocs --with mkdocs-material --with mkdocs-mermaid2-plugin --with mkdocs-swagger-ui-tag

- name: Download OpenAPI spec
run: |
curl -s https://api.integr8scode.cc/openapi.json | \
jq '. + {servers: [{url: "https://api.integr8scode.cc", description: "Production"}]}' \
> docs/reference/openapi.json
- name: Install backend dependencies
run: cd backend && uv sync --frozen

- name: Generate OpenAPI spec
run: ./deploy.sh openapi

- name: Build documentation
run: uv tool run mkdocs build --strict

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v4
Comment thread
HardMax71 marked this conversation as resolved.
with:
path: site/

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mypy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v6

- name: Set up uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "backend/uv.lock"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v6

- name: Set up uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "backend/uv.lock"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v6

- name: Set up uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "backend/uv.lock"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
run: |
echo "Pre-pulling base images to speed up builds..."
docker pull python:3.12-slim &
docker pull ghcr.io/astral-sh/uv:0.9.17 &
docker pull ghcr.io/astral-sh/uv:0.9.18 &
docker pull alpine:latest &
docker pull confluentinc/cp-kafka:7.5.0 &
docker pull confluentinc/cp-zookeeper:7.5.0 &
Expand Down Expand Up @@ -161,7 +161,7 @@ jobs:
kubectl get rolebindings -n default

- name: Set up uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "backend/uv.lock"
Expand Down Expand Up @@ -216,7 +216,7 @@ jobs:

- name: Upload logs
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: integration-test-logs
path: logs/
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
/lib/
/lib64/
parts/
sdist/
var/
Expand Down
50 changes: 26 additions & 24 deletions backend/app/api/routes/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime, timedelta, timezone
from typing import Dict, Union
from uuid import uuid4

from dishka import FromDishka
Expand All @@ -12,7 +11,13 @@
from app.core.utils import get_client_ip
from app.db.repositories import UserRepository
from app.domain.user import User as DomainAdminUser
from app.schemas_pydantic.user import UserCreate, UserResponse
from app.schemas_pydantic.user import (
LoginResponse,
MessageResponse,
TokenValidationResponse,
UserCreate,
UserResponse,
)
from app.services.auth_service import AuthService
from app.settings import get_settings

Expand All @@ -21,13 +26,13 @@
route_class=DishkaRoute)


@router.post("/login")
@router.post("/login", response_model=LoginResponse)
async def login(
request: Request,
response: Response,
user_repo: FromDishka[UserRepository],
form_data: OAuth2PasswordRequestForm = Depends(),
) -> Dict[str, str]:
) -> LoginResponse:
logger.info(
"Login attempt",
extra={
Expand Down Expand Up @@ -112,14 +117,12 @@ async def login(
response.headers["Cache-Control"] = "no-store"
response.headers["Pragma"] = "no-cache"

# Return minimal authentication response
# Detailed user info should be fetched from GET /me endpoint
return {
"message": "Login successful",
"username": user.username,
"role": "admin" if user.is_superuser else "user", # Coarse-grained role
"csrf_token": csrf_token
}
return LoginResponse(
message="Login successful",
username=user.username,
role="admin" if user.is_superuser else "user",
csrf_token=csrf_token
)


@router.post("/register", response_model=UserResponse)
Expand Down Expand Up @@ -224,11 +227,11 @@ async def get_current_user_profile(
return current_user


@router.get("/verify-token")
@router.get("/verify-token", response_model=TokenValidationResponse)
async def verify_token(
request: Request,
auth_service: FromDishka[AuthService],
) -> Dict[str, Union[str, bool]]:
) -> TokenValidationResponse:
current_user = await auth_service.get_current_user(request)
logger.info(
"Token verification attempt",
Expand All @@ -249,15 +252,14 @@ async def verify_token(
"user_agent": request.headers.get("user-agent"),
},
)
# Return existing CSRF token from cookie
csrf_token = request.cookies.get("csrf_token", "")

return {
"valid": True,
"username": current_user.username,
"role": "admin" if current_user.is_superuser else "user", # Coarse-grained role
"csrf_token": csrf_token
}
return TokenValidationResponse(
valid=True,
username=current_user.username,
role="admin" if current_user.is_superuser else "user",
csrf_token=csrf_token
)

except Exception as e:
logger.error(
Expand All @@ -278,11 +280,11 @@ async def verify_token(



@router.post("/logout")
@router.post("/logout", response_model=MessageResponse)
async def logout(
request: Request,
response: Response,
) -> Dict[str, str]:
) -> MessageResponse:
logger.info(
"Logout attempt",
extra={
Expand Down Expand Up @@ -312,4 +314,4 @@ async def logout(
},
)

return {"message": "Logout successful"}
return MessageResponse(message="Logout successful")
2 changes: 1 addition & 1 deletion backend/app/core/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ async def get_redis_client(self, settings: Settings) -> AsyncIterator[redis.Redi
socket_timeout=5,
)
# Test connection
await client.ping()
await client.execute_command("PING")
logger.info(
f"Redis connected: {settings.REDIS_HOST}:{settings.REDIS_PORT}/{settings.REDIS_DB}"
)
Expand Down
20 changes: 20 additions & 0 deletions backend/app/schemas_pydantic/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,23 @@ class MessageResponse(BaseModel):
model_config = ConfigDict(
from_attributes=True
)


class LoginResponse(BaseModel):
"""Response model for successful login"""
message: str
username: str
role: str
csrf_token: str

model_config = ConfigDict(from_attributes=True)


class TokenValidationResponse(BaseModel):
"""Response model for token validation"""
valid: bool
username: str
role: str
csrf_token: str

model_config = ConfigDict(from_attributes=True)
2 changes: 1 addition & 1 deletion backend/app/services/idempotency/redis_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,4 @@ async def aggregate_status_counts(self, key_prefix: str) -> dict[str, int]:
return counts

async def health_check(self) -> None:
await self._r.ping()
await self._r.execute_command("PING")
10 changes: 5 additions & 5 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ dependencies = [
"mdurl==0.1.2",
"motor==3.6.0",
"msgpack==1.1.0",
"multidict==6.6.3",
"multidict==6.7.0",
"oauthlib==3.2.2",
"opentelemetry-api==1.22.0",
"opentelemetry-exporter-otlp==1.22.0",
Expand Down Expand Up @@ -101,7 +101,7 @@ dependencies = [
"python-multipart==0.0.18",
"PyYAML==6.0.2",
"pyzmq==26.2.0",
"redis==5.2.1",
"redis==7.1.0",
"regex==2025.8.29",
"requests==2.32.3",
"requests-oauthlib==2.0.0",
Expand Down Expand Up @@ -134,7 +134,7 @@ packages = ["app", "workers"]

[dependency-groups]
dev = [
"coverage==7.6.2",
"coverage==7.13.0",
"hypothesis==6.103.4",
"iniconfig==2.0.0",
"matplotlib==3.9.2",
Expand All @@ -143,10 +143,10 @@ dev = [
"pipdeptree==2.23.4",
"pluggy==1.5.0",
"pytest==8.3.3",
"pytest-asyncio==0.24.0",
"pytest-asyncio==1.3.0",
"pytest-cov==5.0.0",
"pytest-xdist==3.6.1",
"ruff==0.12.7",
"ruff==0.14.9",
"types-cachetools==6.2.0.20250827",
"types-confluent-kafka==1.3.6",
]
Expand Down
6 changes: 3 additions & 3 deletions backend/tests/fixtures/real_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ async def connect_redis(self, host: str = "localhost", port: int = 6379, db: int
socket_timeout=5
)
# Verify connection
await self.redis_client.ping()
await self.redis_client.execute_command("PING")
# Clear test namespace
await self.redis_client.flushdb()
return self.redis_client
Expand Down Expand Up @@ -328,13 +328,13 @@ async def ensure_services_running():
# Check Redis
try:
r = redis.Redis(host="localhost", port=6379, socket_connect_timeout=5)
await r.ping()
await r.execute_command("PING")
await r.aclose()
except Exception:
print("Starting Redis...")
subprocess.run(["docker-compose", "up", "-d", "redis"], check=False)
await wait_for_service(
lambda: redis.Redis(host="localhost", port=6379).ping(),
lambda: redis.Redis(host="localhost", port=6379).execute_command("PING"),
service_name="Redis"
)

Expand Down
17 changes: 7 additions & 10 deletions backend/tests/unit/events/test_event_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,20 @@ async def handler(ev: BaseEvent) -> None: # noqa: ARG001
assert len(disp.get_handlers(EventType.EXECUTION_REQUESTED)) == 1


def test_dispatch_metrics_processed_and_skipped(event_loop) -> None: # type: ignore[no-redef]
async def test_dispatch_metrics_processed_and_skipped() -> None:
disp = EventDispatcher()
called = {"n": 0}

@disp.register(EventType.EXECUTION_REQUESTED)
async def handler(_: BaseEvent) -> None:
called["n"] += 1

async def run() -> None:
await disp.dispatch(make_event())
# Dispatch event with no handlers (different type)
# Reuse base event but fake type by replacing value
e = make_event()
e.event_type = EventType.EXECUTION_FAILED # type: ignore[attr-defined]
await disp.dispatch(e)

event_loop.run_until_complete(run())
await disp.dispatch(make_event())
# Dispatch event with no handlers (different type)
# Reuse base event but fake type by replacing value
e = make_event()
e.event_type = EventType.EXECUTION_FAILED # type: ignore[attr-defined]
await disp.dispatch(e)

metrics = disp.get_metrics()
assert called["n"] == 1
Expand Down
Loading
Loading