name: e2e-test description: Run E2E test scenarios against running services. Use for happy path testing, unhappy flows, debugging, or when user says "otestuj", "proved test", "zkus flow". allowed-tools: Bash, Read, Glob, Grep, WebFetch, AskUserQuestion
E2E Test Runner
Execute end-to-end test scenarios against running microservices with automatic debugging and reporting.
Usage
/e2e-test # Interactive - asks what to test
/e2e-test happy # Happy path: create order flow
/e2e-test unhappy # Unhappy path: out of stock, invalid data
/e2e-test cancel # Cancel order flow
/e2e-test debug # Just show service status and debug info
/e2e-test trace <corr-id> # Trace request across services by CorrelationId
/e2e-test <custom scenario> # Describe what you want to test
Input
$ARGUMENTS- Test scenario to run or empty for interactive mode
Architecture Reference
Services
| Service | Purpose | API Base | gRPC |
|---|---|---|---|
| Gateway | Reverse proxy (YARP) | /api/* | - |
| Product API | Product catalog & stock | /api/products | ProductService |
| Order API | Order management | /api/orders | - |
| Notification | Email notifications | - (consumer only) | - |
| Analytics | Order tracking | - (consumer only) | - |
Databases (PostgreSQL)
| Database | Tables | Purpose |
|---|---|---|
productdb | Product, Stock, StockReservation, OutboxMessage, InboxState | Product catalog & stock |
orderdb | Order, OrderItem, OutboxMessage, OutboxState, InboxState | Orders + outbox |
notificationdb | ProcessedMessages | Inbox pattern (note: plural "Messages") |
Stock Behavior (IMPORTANT)
Stock quantity is NOT decreased when orders are created. Instead:
- A
StockReservationrecord is created withStatus=0(Active) - When order is cancelled, reservation changes to
Status=1(Released) - The
Stock.Quantityfield represents total inventory, not available inventory
Message Flow
Order API → [gRPC] → Product API (ReserveStock)
↓
OrderConfirmedEvent → [RabbitMQ] → Notification + Analytics
CRITICAL: Gateway-First Rule
ALL API calls in E2E tests MUST go through the Gateway. This is non-negotiable.
✓ CORRECT: curl http://localhost:$GATEWAY_PORT/api/products
✗ WRONG: curl http://localhost:$PRODUCT_PORT/api/products
Why:
- Gateway is the only entry point in production
- E2E tests must simulate real-world usage
- Tests the full stack: YARP proxy, correlation ID propagation, routing
- Catches Gateway-specific issues (routing, headers, timeouts)
When direct service access is allowed:
debugscenario only - to compare Gateway vs direct response- Troubleshooting when Gateway returns error but you need to verify backend works
- Never in
happy,unhappy,cancelscenarios
Process
Phase 0: Service Mode Selection
ALWAYS start here. Use AskUserQuestion to ask the user how they want to manage services:
Question: "How should I manage services for this E2E test?"
Options:
-
Manual (Recommended) - "I'll run the services myself or they're already running"
- User controls AppHost in their own terminal
- Skill only discovers existing services
- User responsible for starting/stopping
-
Automatic - "Start AppHost as a background process"
- Skill starts
dotnet run --project src/AppHostin background usingrun_in_background: true - Skill waits for services to be healthy
- Skill offers to stop AppHost at the end using
TaskStop
- Skill starts
Based on selection:
- Manual mode: Proceed to Phase 1 (Environment Discovery)
- Automatic mode:
- Start AppHost in background:
Usedotnet run --project src/AppHostrun_in_background: trueparameter - Wait 15-30 seconds for services to start
- Run discovery to verify services are healthy
- If services not healthy after 60s, show logs and ask user what to do
- Remember the task_id - you'll need it for cleanup at the end
- Start AppHost in background:
Phase 1: Environment Discovery
Run ./tools/e2e-test/discover.sh to get:
- Running services - ports dynamically assigned by Aspire
- Database connection - PostgreSQL container and credentials
- Message broker - RabbitMQ management URL
- Health status - all service health endpoints
If services are not running:
- Manual mode: Inform user:
Services not detected. Start with: dotnet run --project src/AppHost - Automatic mode: Check AppHost logs for errors, report to user
Phase 2: Scenario Planning
Based on $ARGUMENTS, plan the test scenario:
happy - Order Happy Path
All API calls via Gateway ($GATEWAY_PORT):
- GET
/api/productsvia Gateway → pick one with stock > 0 - POST
/api/ordersvia Gateway → create order (see API Contract below) - Verify: Order status = Confirmed (via Gateway GET
/api/orders/{id}) - Verify: StockReservation created (Status=0) - DB query
- Verify: Notification inbox has record (
ProcessedMessagestable) - DB query - Check logs for complete flow
API Contract - Create Order:
{
"customerId": "guid",
"customerEmail": "email@example.com",
"items": [{
"productId": "guid",
"quantity": 2
}]
}
unhappy - Failure Scenarios
All API calls via Gateway ($GATEWAY_PORT):
- Out of stock: POST
/api/ordersvia Gateway with quantity > available - Invalid product: POST
/api/ordersvia Gateway with non-existent productId - Invalid data: POST
/api/ordersvia Gateway with missing required fields - Verify: Appropriate error responses from Gateway
- Verify: No side effects (stock unchanged, no events) - DB queries
cancel - Order Cancellation
All API calls via Gateway ($GATEWAY_PORT):
- Create order first via Gateway (happy path steps 1-2)
- POST
/api/orders/{orderId}/cancelvia Gateway (see API Contract below) - Verify: Order status = Cancelled (via Gateway GET
/api/orders/{id}) - Verify: StockReservation status changed to 1 (Released) - DB query
- Verify: Cancellation notification processed (
OrderCancelledConsumer) - DB query
API Contract - Cancel Order:
POST /api/orders/{orderId}/cancel
Content-Type: application/json
{"reason": "Customer requested cancellation"} # Body is REQUIRED!
debug - Service Debug Info
This is the only scenario where direct service access is allowed (for comparison/troubleshooting).
- Show all service ports and URLs (Gateway + backend services)
- Show database connection info
- Show recent logs (last 20 lines per service)
- Show message queue status (RabbitMQ)
- Show gRPC connectivity status
- Check service discovery configuration
- (Optional) Compare Gateway response vs direct service response
trace <correlation-id> - Distributed Request Tracing
- Run
./tools/e2e-test/trace-correlation.sh <correlation-id> - Display chronologically sorted logs from all services
- Highlight errors and warnings
- Show service flow visualization
Options:
--all-logs- Search all log files, not just latest--json- Output as JSON for further processing
Example:
/e2e-test trace 228617a4-175a-4384-a8e2-ade916a78c3f
Output shows:
- Service-colored log entries (gateway=cyan, order=green, product=yellow, etc.)
- Chronological order across all services
- Error highlighting in red, warnings in yellow
Phase 3: Execution
Execute scenario step by step. After each step:
- Log the action taken
- Verify expected outcome
- If failure detected → STOP and consult user
REMEMBER: All API calls go through Gateway! (see Gateway-First Rule above)
Use these helpers:
# Service discovery - saves ports to .env file
./tools/e2e-test/discover.sh
source ./tools/e2e-test/.env
# API calls - ALWAYS use Gateway port!
curl -s "http://localhost:$GATEWAY_PORT/api/products" | jq '.'
curl -s "http://localhost:$GATEWAY_PORT/api/orders" | jq '.'
curl -s -X POST "http://localhost:$GATEWAY_PORT/api/orders" -H "Content-Type: application/json" -d '...'
# WRONG - never call services directly in E2E tests:
# curl -s "http://localhost:$PRODUCT_PORT/api/products" # ✗ DON'T DO THIS
# curl -s "http://localhost:$ORDER_PORT/api/orders" # ✗ DON'T DO THIS
# Database queries - get password first, then query
PG_PASS=$(docker exec <container> printenv POSTGRES_PASSWORD)
docker exec -e PGPASSWORD="$PG_PASS" <container> psql -U postgres -d <db> -c '<SQL>'
# Example:
PG_PASS=$(docker exec postgres-4cdf07e3 printenv POSTGRES_PASSWORD)
docker exec -e PGPASSWORD="$PG_PASS" postgres-4cdf07e3 psql -U postgres -d productdb -c 'SELECT * FROM "StockReservation";'
# Log inspection
./tools/e2e-test/logs.sh <service> [lines]
# Trace correlation ID
./tools/e2e-test/trace-correlation.sh <correlation-id>
Finding Service Ports Manually
If discover.sh fails to find services, use this:
# List all dotnet processes with ports
lsof -i -P -n | grep -E "EShop\.(Ord|Pro|Gat)" | grep LISTEN
# Typical output:
# EShop.Ord 45956 ... TCP 127.0.0.1:49814 (LISTEN) <- Order API HTTP
# EShop.Pro 45955 ... TCP 127.0.0.1:49815 (LISTEN) <- Product API HTTP
# EShop.Gat 45954 ... TCP 127.0.0.1:49818 (LISTEN) <- Gateway HTTP
Phase 4: Reporting
Generate structured report:
═══════════════════════════════════════════════════════
E2E TEST REPORT: <scenario name>
═══════════════════════════════════════════════════════
ENVIRONMENT
Gateway: http://localhost:XXXXX ✓ ← All API calls go here
PostgreSQL: localhost:XXXXX ✓
RabbitMQ: localhost:XXXXX ✓
Backend services (for reference only):
Order API: http://localhost:XXXXX ✓
Product API: http://localhost:XXXXX ✓
SCENARIO: <description>
STEPS EXECUTED
[✓] Step 1: Get products
→ Found 10 products, selected "Cable Management Kit" (stock: 100)
[✓] Step 2: Create order
→ POST /api/orders → 201 Created
→ OrderId: abc-123-def
[✓] Step 3: Verify order status
→ DB: Order.Status = 1 (Confirmed)
[✗] Step 4: Verify stock decreased
→ Expected: 98, Actual: 100
→ FAILURE: Stock not reserved
LOGS (relevant entries)
[Order.API 21:00:10] Creating order for customer X
[Order.API 21:00:10] ERROR: gRPC call failed - No address resolver
DIAGNOSIS
Problem: gRPC service discovery not configured
Location: src/Common/EShop.ServiceClients/Extensions/ServiceCollectionExtensions.cs
Fix: Add .AddServiceDiscovery() to gRPC client registration
RESULT: FAILED (Step 4)
═══════════════════════════════════════════════════════
Error Handling
When an error blocks the scenario:
- STOP execution immediately
- Gather diagnostic info:
- Recent logs from affected service
- Database state
- Error response details
- Present to user with options:
⚠️ Test blocked by error at Step X
Error: <description>
Service: <service name>
Log excerpt:
<relevant log lines>
Options:
1. Attempt to fix the issue (I'll suggest a fix)
2. Skip this step and continue
3. Abort test and show partial report
4. Debug mode - show all diagnostic info
Wait for user decision before proceeding.
Key Validation Points
Note: All API calls in these validation points go through Gateway.
Order Creation (Happy Path)
| Check | Query/Command | Expected |
|---|---|---|
| API Response | POST $GATEWAY_PORT/api/orders | 200, status: "Confirmed" |
| Get Order | GET $GATEWAY_PORT/api/orders/{id} | status: "Confirmed" |
| Order in DB | SELECT * FROM "Order" WHERE "Id"='X' | Status = 1 (Confirmed) |
| Reservation created | SELECT * FROM "StockReservation" WHERE "OrderId"='X' | 1 row, Status=0 |
| Stock unchanged | SELECT "Quantity" FROM "Stock" | Same as before (stock is NOT decreased) |
| Outbox processed | SELECT * FROM "OutboxMessage" | 0 pending (processed) |
| Notification inbox | SELECT * FROM "ProcessedMessages" | OrderConfirmedConsumer row |
Order Cancellation
| Check | Query/Command | Expected |
|---|---|---|
| API Response | POST $GATEWAY_PORT/api/orders/{id}/cancel | 200, status: "Cancelled" |
| Get Order | GET $GATEWAY_PORT/api/orders/{id} | status: "Cancelled" |
| Order in DB | SELECT * FROM "Order" WHERE "Id"='X' | Status = 3 (Cancelled) |
| Reservation released | SELECT * FROM "StockReservation" WHERE "OrderId"='X' | Status=1 (Released) |
| Notification inbox | SELECT * FROM "ProcessedMessages" | OrderCancelledConsumer row |
Stock Low Alert
| Check | Query/Command | Expected |
|---|---|---|
| Triggered when | Reservation makes available < threshold | StockLowEvent published |
| Notification inbox | SELECT * FROM "ProcessedMessages" | StockLowConsumer row |
Log Patterns to Verify
| Service | Pattern | Meaning |
|---|---|---|
| Order.API | Creating order for customer | Command received |
| Order.API | Publishing OrderConfirmedEvent | Event dispatched |
| Product.API | ReserveStock request received | gRPC call arrived |
| Product.API | Stock reserved successfully | Reservation complete |
| Notification | Consuming OrderConfirmedEvent | Event received |
| Notification | Sending email to | Email triggered |
RabbitMQ Diagnostics
Use ./tools/e2e-test/rabbitmq.sh for message broker debugging:
./tools/e2e-test/rabbitmq.sh status # Overview
./tools/e2e-test/rabbitmq.sh queues # Queue status with message counts
./tools/e2e-test/rabbitmq.sh connections # Active service connections
./tools/e2e-test/rabbitmq.sh consumers # Consumer registrations
./tools/e2e-test/rabbitmq.sh messages # Pending message analysis
RabbitMQ Validation Points
| Check | What to look for | Issue if... |
|---|---|---|
| Messages Ready | Should be 0 after processing | > 0 = stuck messages, consumer issue |
| Messages Unacked | Should be 0 or low | High = slow consumer or stuck processing |
| Connections | Order, Notification, Analytics | Missing = service not connected |
| Consumers | At least 2 per event type | 0 = no one listening |
| Dead Letter | Should be empty | Messages = repeated failures |
Expected Queues (after first message)
order-confirmed - OrderConfirmedEvent consumers
order-rejected - OrderRejectedEvent consumers
order-cancelled - OrderCancelledEvent consumers
stock-low - StockLowEvent consumers
gRPC Diagnostics
Use ./tools/e2e-test/grpc.sh for inter-service communication debugging:
./tools/e2e-test/grpc.sh status # Port and connectivity check
./tools/e2e-test/grpc.sh list # List services (requires grpcurl)
./tools/e2e-test/grpc.sh test # Test gRPC calls
./tools/e2e-test/grpc.sh discovery # Service discovery configuration
gRPC Services
| Service | Proto | Methods |
|---|---|---|
ProductService | product.proto | GetProducts, ReserveStock, ReleaseStock |
gRPC Validation Points
| Check | How | Expected |
|---|---|---|
| Product service reachable | grpc.sh status | HTTP 200 on health, gRPC port open |
| Service discovery | grpc.sh discovery | AddServiceDiscovery() configured |
| Proto matches | Compare proto file | Same version client/server |
Cleanup After Testing
After completing tests, offer cleanup options based on the mode selected in Phase 0:
Ask User About Cleanup
Manual mode:
Test completed. Cleanup options:
1. Keep everything running (for further testing)
2. Reset test data (purge queues, clear orders)
Note: Stop services manually with Ctrl+C in your AppHost terminal.
What would you like to do?
Automatic mode (AppHost started by skill):
Test completed. Cleanup options:
1. Keep AppHost running (for further testing)
2. Stop AppHost (terminate background process)
3. Reset test data only (keep services running)
4. Full cleanup (stop AppHost + reset data)
What would you like to do?
Use TaskStop with the saved task_id to stop the background AppHost process.
Cleanup Commands
Use ./tools/e2e-test/cleanup.sh for easy cleanup:
./tools/e2e-test/cleanup.sh status # See what needs cleaning
./tools/e2e-test/cleanup.sh data # Clear test orders, reservations
./tools/e2e-test/cleanup.sh queues # Purge RabbitMQ queues
./tools/e2e-test/cleanup.sh logs # Remove logs older than 7 days
./tools/e2e-test/cleanup.sh env # Remove generated .env
./tools/e2e-test/cleanup.sh services # Kill all running EShop services
./tools/e2e-test/cleanup.sh all # Full cleanup (except services)
Kill Services
Use ./tools/e2e-test/kill-services.sh to kill all running EShop services:
./tools/e2e-test/kill-services.sh # Show running services
./tools/e2e-test/kill-services.sh kill # Kill with confirmation
./tools/e2e-test/kill-services.sh --force # Kill without confirmation
This script finds and terminates all processes matching:
EShop.*(AppHost, common libs)Order.API,Products.API,Gateway.APINotification.API,Analytics.APIDatabaseMigration
Or manually:
# Stop Aspire (if started by this session)
# Note: AppHost runs in foreground, Ctrl+C stops it
# Reset databases (clear test orders, keep products)
./tools/reset-db.sh
# Purge RabbitMQ queues (remove stuck messages)
./tools/e2e-test/rabbitmq.sh purge <queue-name>
Test Data Cleanup SQL
-- Clear orders (orderdb)
DELETE FROM "OrderItem";
DELETE FROM "Order";
DELETE FROM "OutboxMessage";
DELETE FROM "OutboxState";
DELETE FROM "InboxState";
-- Clear stock reservations (productdb)
DELETE FROM "StockReservation";
-- Note: Stock.Quantity doesn't need reset - it's not modified by orders
-- Clear processed messages (notificationdb) - note plural "Messages"
DELETE FROM "ProcessedMessages";
When to Clean Up
| Scenario | Recommended Cleanup |
|---|---|
| Single test run | Option 1 (keep running) |
| End of testing session | Option 2 (stop services) |
| Tests created bad data | Option 3 (reset data) |
| Fresh start needed | Option 4 (full cleanup) |
Important Notes
- Never auto-cleanup without asking user
- Preserve logs - useful for debugging issues found during tests
- Database reset uses
./tools/reset-db.shwhich re-seeds products - RabbitMQ queues auto-recreate when services restart
- Automatic mode cleanup - if you started AppHost in background, ALWAYS offer to stop it at the end using
TaskStopwith the savedtask_id - Manual mode - user manages services, don't attempt to stop them
Files Reference
| File | Purpose |
|---|---|
./tools/e2e-test/discover.sh | Service discovery (ports, credentials) |
./tools/e2e-test/db-query.sh | Database queries |
./tools/e2e-test/logs.sh | Log inspection |
./tools/e2e-test/api.sh | API call helper |
./tools/e2e-test/rabbitmq.sh | RabbitMQ diagnostics |
./tools/e2e-test/grpc.sh | gRPC diagnostics |
./tools/e2e-test/trace-correlation.sh | Distributed tracing by CorrelationId |
./tools/e2e-test/cleanup.sh | Test cleanup (default: all) |
./tools/e2e-test/kill-services.sh | Kill all running EShop services |
./tools/reset-db.sh | Database reset script |
src/Services/*/logs/*.log | Service log files |
http://localhost:PORT/swagger | API documentation |
Technical Notes
StockReservation Table Schema
ReservedAt- timestamp when reservation was madeExpiresAt- when reservation expiresStatus: 0 = Active, 1 = Released
Process Names in lsof
Aspire services use truncated names:
EShop.Ord/Order.APIEShop.Pro/Products.EShop.Gat/Gateway.A
The discover.sh script searches for both naming patterns.