Skip to content

Commit 24c61a3

Browse files
committed
Add Error Testing Lab feature for simulated error handling
1 parent 36cb4e8 commit 24c61a3

11 files changed

Lines changed: 455 additions & 2 deletions

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,20 @@ Additional features:
109109
- **Fully responsive** layout (mobile, tablet, desktop) with safe-area support for iPhone
110110
- **Animated space background** with twinkling stars and shooting stars
111111
- **Image lightbox** for all galleries
112+
- **Error Testing Lab** — Easter egg to demonstrate error handling (triple-click SPACEX in header)
113+
114+
## Error Testing Lab
115+
116+
The dashboard includes an **Error Testing Lab** to demonstrate how errors are handled across the stack. Useful for QA, demos, and onboarding.
117+
118+
**How to open:** Triple-click the "SPACEX" brand in the header (three clicks within ~500 ms).
119+
120+
**What it demonstrates:**
121+
- **ErrorState** — Inline error panel with RETRY button (used across all views)
122+
- **NotificationToast** — Toast notifications for non-blocking errors
123+
- **API errors** — Real HTTP responses (404, 500, 502, 503, timeout) from the backend
124+
125+
The backend endpoint `GET /api/dev/trigger-error?code=404|500|502|503|timeout` returns simulated errors. The timeout variant waits 5 seconds before responding. See [Frontend README](frontend/README.md#error-testing-lab-easter-egg) and [Backend README](backend/README.md#error-testing-dev-endpoint) for details.
112126

113127
## API Endpoints
114128

@@ -135,6 +149,7 @@ Additional features:
135149
| GET | `/api/ai/fun-fact` | Random AI-generated SpaceX fun fact |
136150
| GET | `/api/notifications/stream` | Server-Sent Events stream for real-time notifications |
137151
| POST | `/api/notifications/send` | Send test notification (admin/internal) |
152+
| GET | `/api/dev/trigger-error` | Easter egg: simulated errors (404, 500, 502, 503, timeout) for error-handling demos |
138153

139154
For detailed query parameters and response schemas, see the [Backend README](backend/README.md).
140155

backend/README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,24 @@ The AI service uses Groq (Llama 3.3 70B) to generate responses grounded in real
214214
| GET | `/api/notifications/stream` | SSE stream for real-time notifications |
215215
| POST | `/api/notifications/send` | Send test notification |
216216

217+
### Error Testing (Dev Endpoint)
218+
219+
| Method | Path | Description |
220+
|--------|--------------------------|--------------------------------------------------|
221+
| GET | `/api/dev/trigger-error` | Simulated errors for error-handling demos |
222+
223+
**Query parameters:**
224+
225+
| Param | Type | Values | Description |
226+
|--------|--------|---------------------------|--------------------------------------|
227+
| `code` | string | `404`, `500`, `502`, `503`, `timeout` | Error type to simulate (default: `500`) |
228+
229+
**Behavior:**
230+
- `404`, `500`, `502`, `503` — Returns immediately with the corresponding HTTP status and structured body `{ code, message, trace_id }`
231+
- `timeout` — Waits 5 seconds, then returns 504 Gateway Timeout
232+
233+
**Use case:** The frontend Error Testing Lab (triple-click SPACEX) uses this endpoint to demonstrate `ErrorState`, `NotificationToast`, and API error handling. Also useful for manual testing via curl or browser. Rate limited to 30 requests per minute per IP.
234+
217235
### Error Response Format
218236

219237
All errors follow a consistent structure:
@@ -268,7 +286,7 @@ pytest --cov=app --cov-report=term-missing
268286
| Total tests | **203** |
269287
| Code coverage | **96.12%** |
270288
| Enforced minimum | **90%** (`--cov-fail-under=90` in CI) |
271-
| Test files | 24 (routes, services, cache, middleware, rate_limit, AI, notifications, etc.) |
289+
| Test files | 25 (routes, services, cache, middleware, rate_limit, AI, notifications, dev, etc.) |
272290

273291
Tests cover all 14 API route modules, all 15 service modules, error handling, caching behavior, and the AI service. Requires Redis running locally — the CI pipeline uses a Redis service container.
274292

@@ -286,7 +304,7 @@ ruff format . # Auto-format
286304
backend/
287305
├── app/
288306
│ ├── api/
289-
│ │ ├── routes/ # One file per resource (14 route modules)
307+
│ │ ├── routes/ # One file per resource (15 route modules, including dev.py)
290308
│ │ ├── middleware.py # Request ID, structured logging, rate limiting (120 req/min)
291309
│ │ └── rate_limit.py # Per-endpoint rate limiting
292310
│ ├── cache/

backend/app/api/routes/dev.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Easter egg: trigger simulated errors for testing error handling in the frontend."""
2+
3+
import asyncio
4+
5+
from fastapi import APIRouter, Request
6+
from starlette.responses import JSONResponse
7+
8+
from app.api.rate_limit import check_rate_limit
9+
10+
router = APIRouter(prefix="/api/dev", tags=["dev"])
11+
12+
ERROR_MESSAGES: dict[str, tuple[int, str, str]] = {
13+
"404": (404, "RESOURCE_NOT_FOUND", "Simulated 404 — Resource not found"),
14+
"500": (500, "INTERNAL_ERROR", "Simulated 500 — Internal server error"),
15+
"502": (502, "BAD_GATEWAY", "Simulated 502 — Bad gateway"),
16+
"503": (503, "SERVICE_UNAVAILABLE", "Simulated 503 — Service unavailable"),
17+
"timeout": (504, "GATEWAY_TIMEOUT", "Simulated timeout — Request took too long"),
18+
}
19+
20+
21+
@router.get("/trigger-error")
22+
async def trigger_error(request: Request, code: str = "500"):
23+
"""Return a simulated error response for testing frontend error handling.
24+
25+
Query params:
26+
code: 404 | 500 | 502 | 503 | timeout
27+
28+
Use the Error Lab panel (triple-click SPACEX in header) to trigger from the UI.
29+
"""
30+
await check_rate_limit(request, prefix="rl:dev", max_requests=30, window_seconds=60)
31+
32+
entry = ERROR_MESSAGES.get(code.lower(), ERROR_MESSAGES["500"])
33+
status_code, error_code, message = entry
34+
35+
if code.lower() == "timeout":
36+
await asyncio.sleep(5) # Simulated delay; client timeout (30s) still allows response
37+
38+
trace_id = getattr(request.state, "trace_id", "unknown")
39+
return JSONResponse(
40+
status_code=status_code,
41+
content={"code": error_code, "message": message, "trace_id": trace_id},
42+
)

backend/app/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
ai,
1212
cores,
1313
dashboard,
14+
dev,
1415
economics,
1516
emissions,
1617
health,
@@ -72,3 +73,4 @@ async def lifespan(app: FastAPI):
7273
app.include_router(emissions.router)
7374
app.include_router(ai.router)
7475
app.include_router(notifications.router)
76+
app.include_router(dev.router)

backend/tests/test_dev_routes.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Tests for the dev/trigger-error easter egg endpoint."""
2+
3+
import pytest
4+
5+
6+
@pytest.mark.asyncio
7+
async def test_trigger_error_404(client):
8+
"""Returns 404 with structured error body."""
9+
resp = await client.get("/api/dev/trigger-error?code=404")
10+
assert resp.status_code == 404
11+
data = resp.json()
12+
assert data["code"] == "RESOURCE_NOT_FOUND"
13+
assert "404" in data["message"]
14+
assert "trace_id" in data
15+
16+
17+
@pytest.mark.asyncio
18+
async def test_trigger_error_500(client):
19+
"""Returns 500 with structured error body."""
20+
resp = await client.get("/api/dev/trigger-error?code=500")
21+
assert resp.status_code == 500
22+
data = resp.json()
23+
assert data["code"] == "INTERNAL_ERROR"
24+
assert "500" in data["message"]
25+
assert "trace_id" in data
26+
27+
28+
@pytest.mark.asyncio
29+
async def test_trigger_error_502(client):
30+
"""Returns 502 with structured error body."""
31+
resp = await client.get("/api/dev/trigger-error?code=502")
32+
assert resp.status_code == 502
33+
data = resp.json()
34+
assert data["code"] == "BAD_GATEWAY"
35+
36+
37+
@pytest.mark.asyncio
38+
async def test_trigger_error_503(client):
39+
"""Returns 503 with structured error body."""
40+
resp = await client.get("/api/dev/trigger-error?code=503")
41+
assert resp.status_code == 503
42+
data = resp.json()
43+
assert data["code"] == "SERVICE_UNAVAILABLE"
44+
45+
46+
@pytest.mark.asyncio
47+
async def test_trigger_error_unknown_defaults_to_500(client):
48+
"""Unknown code defaults to 500."""
49+
resp = await client.get("/api/dev/trigger-error?code=invalid")
50+
assert resp.status_code == 500
51+
data = resp.json()
52+
assert data["code"] == "INTERNAL_ERROR"

frontend/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Custom D3.js visualizations — each component uses `ResizeObserver` to adapt to
102102
|------------------|-----------------------------------------------------------------------------|
103103
| AiChat | Floating AI assistant with markdown rendering and typewriter effect (lazy-loaded via `defineAsyncComponent`) |
104104
| DataTable | Paginated table with clickable rows |
105+
| ErrorLabPanel | Easter egg panel for triggering simulated errors (triple-click SPACEX) |
105106
| ErrorState | Error message with retry button and `retrying` spinner state |
106107
| FunFact | AI-generated curiosity banner on site load with auto-dismiss (lazy-loaded via `defineAsyncComponent`) |
107108
| ImageLightbox | Full-screen image viewer with keyboard navigation and focus trap |
@@ -181,6 +182,7 @@ frontend/
181182
│ │ └── index.ts # 11 routes with lazy loading
182183
│ ├── stores/
183184
│ │ ├── dashboard.ts
185+
│ │ ├── errorLab.ts
184186
│ │ ├── fleet.ts
185187
│ │ ├── launches.ts
186188
│ │ ├── notifications.ts
@@ -239,6 +241,29 @@ Every view has a tailored loading state that mirrors its actual content structur
239241

240242
The `ErrorState` component includes a `retrying` prop that shows a spinner and "RETRYING..." label during automatic retry operations, avoiding UI flicker.
241243

244+
## Error Testing Lab (Easter Egg)
245+
246+
The dashboard includes a hidden **Error Testing Lab** to demonstrate error handling. See also the [Backend dev endpoint](backend/README.md#error-testing-dev-endpoint).
247+
248+
**How to open:** Triple-click the "SPACEX" brand in the header (three clicks within ~500 ms).
249+
250+
**Panel options:**
251+
- **Show ErrorState** — Simulates an inline error on Overview with a RETRY button
252+
- **Show error toast** — Displays a NotificationToast with type `error` (auto-dismisses after 12 s)
253+
- **API errors (404, 500, 502, 503, timeout)** — Calls the backend `/api/dev/trigger-error` and shows the resulting error on Overview
254+
255+
**Direct API testing** (curl or browser):
256+
257+
```bash
258+
# Local
259+
curl "http://localhost:8000/api/dev/trigger-error?code=404"
260+
curl "http://localhost:8000/api/dev/trigger-error?code=500"
261+
curl "http://localhost:8000/api/dev/trigger-error?code=timeout" # waits 5s
262+
263+
# Production
264+
curl "https://quadsci.juantoledo.com.mx/api/dev/trigger-error?code=404"
265+
```
266+
242267
## Tests
243268

244269
The frontend test suite includes **242 tests** across **47 test files**, covering:

frontend/src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<AiChat />
2525
<FunFact />
2626
<NotificationToast />
27+
<ErrorLabPanel />
2728
</div>
2829
</template>
2930

@@ -32,6 +33,7 @@ import { defineAsyncComponent, onMounted } from 'vue'
3233
import AppHeader from '@/components/layout/AppHeader.vue'
3334
import AppFooter from '@/components/layout/AppFooter.vue'
3435
import RouteProgressBar from '@/components/layout/RouteProgressBar.vue'
36+
import ErrorLabPanel from '@/components/common/ErrorLabPanel.vue'
3537
import { useNotificationStore } from '@/stores/notifications'
3638
3739
const SpaceBackground = defineAsyncComponent(() => import('@/components/layout/SpaceBackground.vue'))

frontend/src/api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,8 @@ export const sendChatMessage = (message: string, history: ChatMessage[]): Promis
7979
export const fetchFunFact = (): Promise<FunFactResponse> =>
8080
api.get('/ai/fun-fact').then((r) => r.data)
8181

82+
/** Trigger a simulated API error (404, 500, 502, 503, timeout) for error-handling demos. */
83+
export const triggerError = (code: string) =>
84+
api.get('/dev/trigger-error', { params: { code } })
85+
8286
export default api

0 commit comments

Comments
 (0)