Run E2E test scenarios against running services. Use for happy path testing, unhappy flows, debugging, or when user says "otestuj", "proved test", "zkus flow".
Execute end-to-end test scenarios against running microservices with automatic debugging and reporting.
/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
$ARGUMENTS - Test scenario to run or empty for interactive mode| 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) | - |
| 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 quantity is NOT decreased when orders are created. Instead:
StockReservation record is created with Status=0 (Active)Status=1 (Released)Stock.Quantity field represents total inventory, not available inventoryOrder API → [gRPC] → Product API (ReserveStock)
↓
OrderConfirmedEvent → [RabbitMQ] → Notification + Analytics
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:
When direct service access is allowed:
debug scenario only - to compare Gateway vs direct responsehappy, unhappy, cancel scenariosALWAYS 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"
Automatic - "Start AppHost as a background process"
dotnet run --project src/AppHost in background using run_in_background: trueTaskStopBased on selection:
dotnet run --project src/AppHost
Use run_in_background: true parameterRun ./tools/e2e-test/discover.sh to get:
If services are not running:
Services not detected. Start with: dotnet run --project src/AppHostBased on $ARGUMENTS, plan the test scenario:
happy - Order Happy PathAll API calls via Gateway ($GATEWAY_PORT):
/api/products via Gateway → pick one with stock > 0/api/orders via Gateway → create order (see API Contract below)/api/orders/{id})ProcessedMessages table) - DB queryAPI Contract - Create Order:
{
"customerId": "guid",
"customerEmail": "[email protected]",
"items": [{
"productId": "guid",
"quantity": 2
}]
}
unhappy - Failure ScenariosAll API calls via Gateway ($GATEWAY_PORT):
/api/orders via Gateway with quantity > available/api/orders via Gateway with non-existent productId/api/orders via Gateway with missing required fieldscancel - Order CancellationAll API calls via Gateway ($GATEWAY_PORT):
/api/orders/{orderId}/cancel via Gateway (see API Contract below)/api/orders/{id})OrderCancelledConsumer) - DB queryAPI Contract - Cancel Order:
POST /api/orders/{orderId}/cancel
Content-Type: application/json
{"reason": "Customer requested cancellation"} # Body is REQUIRED!
debug - Service Debug InfoThis is the only scenario where direct service access is allowed (for comparison/troubleshooting).
trace <correlation-id> - Distributed Request Tracing./tools/e2e-test/trace-correlation.sh <correlation-id>Options:
--all-logs - Search all log files, not just latest--json - Output as JSON for further processingExample:
/e2e-test trace 228617a4-175a-4384-a8e2-ade916a78c3f
Output shows:
Execute scenario step by step. After each step:
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>
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
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)
═══════════════════════════════════════════════════════
When an error blocks the scenario:
⚠️ 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.
Note: All API calls in these validation points go through Gateway.
| 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 |
| 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 |
| Check | Query/Command | Expected |
|---|---|---|
| Triggered when | Reservation makes available < threshold | StockLowEvent published |
| Notification inbox | SELECT * FROM "ProcessedMessages" | StockLowConsumer row |
| 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 |
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
| 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 |
order-confirmed - OrderConfirmedEvent consumers
order-rejected - OrderRejectedEvent consumers
order-cancelled - OrderCancelledEvent consumers
stock-low - StockLowEvent consumers
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
| Service | Proto | Methods |
|---|---|---|
ProductService | product.proto | GetProducts, ReserveStock, ReleaseStock |
| 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 |
After completing tests, offer cleanup options based on the mode selected in Phase 0:
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.
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)
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.APIDatabaseMigrationOr 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>
-- 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";
| 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) |
./tools/reset-db.sh which re-seeds productsTaskStop with the saved task_id| 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 |
ReservedAt - timestamp when reservation was madeExpiresAt - when reservation expiresStatus: 0 = Active, 1 = ReleasedAspire services use truncated names:
EShop.Ord / Order.APIEShop.Pro / Products.EShop.Gat / Gateway.AThe discover.sh script searches for both naming patterns.