Skip to content

Commit f2307de

Browse files
authored
chore/test: add more tests for frontend (#100)
* components to show general coverage for backend and frontend regardless of flags + ignore of test files * regen of api stuff from spec + fixing eslint warnings * custom toasts -> svelte-sonner toasts * custom event source -> event-source-plus * Double Cast Bypasses Type Safety in EventDetailsModal - fix * `any[]` Type for Settings History, JSON.parse Without Try-Catch in Execution SSE (added schemas for sse endpoints) * fixes
1 parent 385c408 commit f2307de

52 files changed

Lines changed: 14399 additions & 4977 deletions

Some content is hidden

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

.github/codecov.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,24 @@ flag_management:
2121
carryforward: true
2222
- name: frontend
2323
carryforward: true
24+
25+
component_management:
26+
individual_components:
27+
- component_id: backend
28+
name: Backend
29+
paths:
30+
- backend/app/**
31+
- component_id: frontend
32+
name: Frontend
33+
paths:
34+
- frontend/src/**
35+
36+
ignore:
37+
- "**/tests/**"
38+
- "**/test/**"
39+
- "**/__tests__/**"
40+
- "**/test_*.py"
41+
- "**/*_test.py"
42+
- "**/*.test.ts"
43+
- "**/*.spec.ts"
44+
- "**/conftest.py"

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@
2626
</a>
2727
</p>
2828
<p align="center">
29-
<a href="https://app.codecov.io/gh/HardMax71/Integr8sCode">
30-
<img src="https://img.shields.io/codecov/c/github/HardMax71/Integr8sCode?label=backend%20cov&logo=codecov" alt="Backend Coverage" />
29+
<a href="https://app.codecov.io/gh/HardMax71/Integr8sCode/components?component=backend">
30+
<img src="https://codecov.io/gh/HardMax71/Integr8sCode/branch/main/graph/badge.svg?component=backend" alt="Backend Coverage" />
3131
</a>
32-
<a href="https://app.codecov.io/gh/HardMax71/Integr8sCode/flags?flag=frontend">
33-
<img src="https://img.shields.io/codecov/c/github/HardMax71/Integr8sCode?flag=frontend&label=frontend%20cov&logo=codecov" alt="Frontend Coverage" />
32+
<a href="https://app.codecov.io/gh/HardMax71/Integr8sCode/components?component=frontend">
33+
<img src="https://codecov.io/gh/HardMax71/Integr8sCode/branch/main/graph/badge.svg?component=frontend" alt="Frontend Coverage" />
3434
</a>
3535
</p>
3636
<p align="center">

backend/app/api/routes/admin/events.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from dishka import FromDishka
55
from dishka.integrations.fastapi import DishkaRoute
66
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query
7-
from fastapi.encoders import jsonable_encoder
87
from fastapi.responses import StreamingResponse
98

109
from app.api.dependencies import admin_user
@@ -44,7 +43,7 @@ async def browse_events(request: EventBrowseRequest, service: FromDishka[AdminEv
4443
)
4544

4645
return EventBrowseResponse(
47-
events=[jsonable_encoder(event) for event in result.events],
46+
events=result.events,
4847
total=result.total,
4948
skip=result.skip,
5049
limit=result.limit,
@@ -135,9 +134,9 @@ async def get_event_detail(event_id: str, service: FromDishka[AdminEventsService
135134
raise HTTPException(status_code=404, detail="Event not found")
136135

137136
return EventDetailResponse(
138-
event=jsonable_encoder(result.event),
139-
related_events=[jsonable_encoder(e) for e in result.related_events],
140-
timeline=[jsonable_encoder(e) for e in result.timeline],
137+
event=result.event,
138+
related_events=result.related_events,
139+
timeline=result.timeline,
141140
)
142141

143142
except HTTPException:

backend/app/api/routes/events.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from dishka import FromDishka
77
from dishka.integrations.fastapi import DishkaRoute
88
from fastapi import APIRouter, Depends, HTTPException, Query, Request
9-
from pydantic import TypeAdapter
109

1110
from app.api.dependencies import admin_user, current_user
1211
from app.core.correlation import CorrelationContext
@@ -21,7 +20,6 @@
2120
EventAggregationRequest,
2221
EventFilterRequest,
2322
EventListResponse,
24-
EventResponse,
2523
EventStatistics,
2624
PublishEventRequest,
2725
PublishEventResponse,
@@ -33,8 +31,6 @@
3331
from app.services.kafka_event_service import KafkaEventService
3432
from app.settings import Settings
3533

36-
_event_response_list_adapter: TypeAdapter[list[EventResponse]] = TypeAdapter(list[EventResponse])
37-
3834
router = APIRouter(prefix="/events", tags=["events"], route_class=DishkaRoute)
3935

4036

@@ -66,7 +62,7 @@ async def get_execution_events(
6662
raise HTTPException(status_code=403, detail="Access denied")
6763

6864
return EventListResponse(
69-
events=_event_response_list_adapter.validate_python(result.events, from_attributes=True),
65+
events=result.events,
7066
total=result.total,
7167
limit=limit,
7268
skip=skip,
@@ -97,7 +93,7 @@ async def get_user_events(
9793
)
9894

9995
return EventListResponse(
100-
events=_event_response_list_adapter.validate_python(result.events, from_attributes=True),
96+
events=result.events,
10197
total=result.total,
10298
limit=limit,
10399
skip=skip,
@@ -134,7 +130,7 @@ async def query_events(
134130
raise HTTPException(status_code=403, detail="Cannot query other users' events")
135131

136132
return EventListResponse(
137-
events=_event_response_list_adapter.validate_python(result.events, from_attributes=True),
133+
events=result.events,
138134
total=result.total,
139135
limit=result.limit,
140136
skip=result.skip,
@@ -161,7 +157,7 @@ async def get_events_by_correlation(
161157
)
162158

163159
return EventListResponse(
164-
events=_event_response_list_adapter.validate_python(result.events, from_attributes=True),
160+
events=result.events,
165161
total=result.total,
166162
limit=limit,
167163
skip=skip,
@@ -190,7 +186,7 @@ async def get_current_request_events(
190186
)
191187

192188
return EventListResponse(
193-
events=_event_response_list_adapter.validate_python(result.events, from_attributes=True),
189+
events=result.events,
194190
total=result.total,
195191
limit=limit,
196192
skip=skip,

backend/app/api/routes/execution.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,15 @@
55
from dishka import FromDishka
66
from dishka.integrations.fastapi import DishkaRoute, inject
77
from fastapi import APIRouter, Depends, Header, HTTPException, Path, Query, Request
8-
from pydantic import TypeAdapter
98

109
from app.api.dependencies import admin_user, current_user
1110
from app.core.tracing import EventAttributes, add_span_attributes
1211
from app.core.utils import get_client_ip
1312
from app.domain.enums.events import EventType
1413
from app.domain.enums.execution import ExecutionStatus
1514
from app.domain.enums.user import UserRole
16-
from app.domain.events.typed import BaseEvent, EventMetadata
15+
from app.domain.events.typed import BaseEvent, DomainEvent, EventMetadata
1716
from app.domain.exceptions import DomainError
18-
from app.schemas_pydantic.events import EventResponse
1917
from app.schemas_pydantic.execution import (
2018
CancelExecutionRequest,
2119
CancelResponse,
@@ -36,8 +34,6 @@
3634
from app.services.kafka_event_service import KafkaEventService
3735
from app.settings import Settings
3836

39-
_event_list_adapter: TypeAdapter[list[EventResponse]] = TypeAdapter(list[EventResponse])
40-
4137
router = APIRouter(route_class=DishkaRoute, tags=["execution"])
4238

4339

@@ -234,18 +230,18 @@ async def retry_execution(
234230
return ExecutionResponse.model_validate(new_result)
235231

236232

237-
@router.get("/executions/{execution_id}/events", response_model=list[EventResponse])
233+
@router.get("/executions/{execution_id}/events", response_model=list[DomainEvent])
238234
async def get_execution_events(
239235
execution: Annotated[ExecutionInDB, Depends(get_execution_with_access)],
240236
event_service: FromDishka[EventService],
241237
event_types: list[EventType] | None = Query(None, description="Event types to filter"),
242238
limit: int = Query(100, ge=1, le=1000),
243-
) -> list[EventResponse]:
239+
) -> list[DomainEvent]:
244240
"""Get all events for an execution."""
245241
events = await event_service.get_events_by_aggregate(
246242
aggregate_id=execution.execution_id, event_types=event_types, limit=limit
247243
)
248-
return _event_list_adapter.validate_python([e.model_dump() for e in events])
244+
return events
249245

250246

251247
@router.get("/user/executions", response_model=ExecutionListResponse)

backend/app/api/routes/sse.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@
44
from sse_starlette.sse import EventSourceResponse
55

66
from app.domain.sse import SSEHealthDomain
7-
from app.schemas_pydantic.sse import ShutdownStatusResponse, SSEHealthResponse
7+
from app.schemas_pydantic.sse import (
8+
ShutdownStatusResponse,
9+
SSEExecutionEventData,
10+
SSEHealthResponse,
11+
SSENotificationEventData,
12+
)
813
from app.services.auth_service import AuthService
914
from app.services.sse.sse_service import SSEService
1015

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

1318

14-
@router.get("/notifications/stream")
19+
@router.get("/notifications/stream", responses={200: {"model": SSENotificationEventData}})
1520
async def notification_stream(
1621
request: Request,
1722
sse_service: FromDishka[SSEService],
@@ -23,7 +28,7 @@ async def notification_stream(
2328
return EventSourceResponse(sse_service.create_notification_stream(user_id=current_user.user_id))
2429

2530

26-
@router.get("/executions/{execution_id}")
31+
@router.get("/executions/{execution_id}", responses={200: {"model": SSEExecutionEventData}})
2732
async def execution_events(
2833
execution_id: str, request: Request, sse_service: FromDishka[SSEService], auth_service: FromDishka[AuthService]
2934
) -> EventSourceResponse:

backend/app/domain/events/typed.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,15 @@ class EventMetadata(AvroBase):
3030
class BaseEvent(AvroBase):
3131
"""Base fields for all domain events."""
3232

33-
model_config = ConfigDict(from_attributes=True)
33+
# Pydantic marks fields with default/default_factory as optional in JSON Schema,
34+
# which generates optional TypeScript types (e.g., `event_id?: string`).
35+
# Since stored events always have these fields, we override the schema to mark them required.
36+
# See: https://github.com/pydantic/pydantic/issues/7209
37+
# See: https://github.com/pydantic/pydantic/discussions/6073
38+
model_config = ConfigDict(
39+
from_attributes=True,
40+
json_schema_extra={"required": ["event_id", "event_type", "event_version", "timestamp", "metadata"]},
41+
)
3442

3543
event_id: str = Field(default_factory=lambda: str(uuid4()))
3644
event_type: EventType

backend/app/schemas_pydantic/admin_events.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from datetime import datetime
2-
from typing import Any, Dict, List
2+
from typing import Dict, List
33

44
from pydantic import BaseModel, ConfigDict, Field, computed_field
55

66
from app.domain.enums.events import EventType
7+
from app.domain.events.event_models import EventSummary
8+
from app.domain.events.typed import DomainEvent
79
from app.schemas_pydantic.events import HourlyEventCountSchema
810
from app.schemas_pydantic.execution import ExecutionResult
911

@@ -55,7 +57,7 @@ class EventReplayRequest(BaseModel):
5557
class EventBrowseResponse(BaseModel):
5658
"""Response model for browsing events"""
5759

58-
events: List[Dict[str, Any]]
60+
events: List[DomainEvent]
5961
total: int
6062
skip: int
6163
limit: int
@@ -64,9 +66,9 @@ class EventBrowseResponse(BaseModel):
6466
class EventDetailResponse(BaseModel):
6567
"""Response model for event detail"""
6668

67-
event: Dict[str, Any]
68-
related_events: List[Dict[str, Any]]
69-
timeline: List[Dict[str, Any]]
69+
event: DomainEvent
70+
related_events: List[EventSummary]
71+
timeline: List[EventSummary]
7072

7173

7274
class EventReplayResponse(BaseModel):
@@ -77,7 +79,7 @@ class EventReplayResponse(BaseModel):
7779
replay_correlation_id: str
7880
session_id: str | None = None
7981
status: str
80-
events_preview: List[Dict[str, Any]] | None = None
82+
events_preview: List[EventSummary] | None = None
8183

8284

8385
class EventReplayStatusResponse(BaseModel):

backend/app/schemas_pydantic/events.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from app.domain.enums.common import Environment, SortOrder
88
from app.domain.enums.events import EventType
9+
from app.domain.events.typed import DomainEvent
910

1011

1112
class HourlyEventCountSchema(BaseModel):
@@ -31,23 +32,21 @@ class EventMetadataResponse(BaseModel):
3132
environment: Environment = Environment.PRODUCTION
3233

3334

34-
class EventResponse(BaseModel):
35-
"""API response schema for events. Captures all event-specific fields via extra='allow'."""
35+
class EventSummaryResponse(BaseModel):
36+
"""Lightweight event summary for lists and related events display."""
3637

37-
model_config = ConfigDict(from_attributes=True, extra="allow")
38+
model_config = ConfigDict(from_attributes=True)
3839

3940
event_id: str
4041
event_type: EventType
41-
event_version: str = "1.0"
4242
timestamp: datetime
4343
aggregate_id: str | None = None
44-
metadata: EventMetadataResponse
4544

4645

4746
class EventListResponse(BaseModel):
4847
model_config = ConfigDict(from_attributes=True)
4948

50-
events: List[EventResponse]
49+
events: List[DomainEvent]
5150
total: int
5251
limit: int
5352
skip: int

backend/app/schemas_pydantic/user.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,10 @@ class RateLimitRuleResponse(BaseModel):
154154
"""Response model for rate limit rule."""
155155

156156
endpoint_pattern: str
157-
group: str
157+
group: EndpointGroup
158158
requests: int
159159
window_seconds: int
160-
algorithm: str
160+
algorithm: RateLimitAlgorithm
161161
burst_multiplier: float = 1.5
162162
priority: int = 0
163163
enabled: bool = True

0 commit comments

Comments
 (0)