15 KiB
Phase 1 Testing & Validation Guide
This guide provides comprehensive testing procedures for validating all Phase 1 event streaming functionality.
Table of Contents
- Prerequisites
- Starting the Sample Application
- Test 1: Workflow Start Semantics
- Test 2: Event Consumer (Broadcast Mode)
- Test 3: Ephemeral Streams
- Test 4: gRPC Streaming
- Test 5: Existing Features (Regression)
- Expected Results Summary
Prerequisites
Required Tools:
- .NET 10 SDK
curl(for HTTP testing)grpcurl(for gRPC testing) - Install:brew install grpcurl(macOS) or download from https://github.com/fullstorydev/grpcurl
Optional Tools:
- Postman or similar REST client
- gRPC UI or BloomRPC for visual gRPC testing
Starting the Sample Application
cd Svrnty.Sample
dotnet run
Expected Output:
=== Svrnty CQRS Sample with Event Streaming ===
gRPC (HTTP/2): http://localhost:6000
- CommandService, QueryService, DynamicQueryService
- EventService (bidirectional streaming)
HTTP API (HTTP/1.1): http://localhost:6001
- Commands: POST /api/command/*
- Queries: GET/POST /api/query/*
- Swagger UI: http://localhost:6001/swagger
Event Streams Configured:
- UserWorkflow stream (ephemeral, at-least-once)
- InvitationWorkflow stream (ephemeral, at-least-once)
Subscriptions Active:
- user-analytics (broadcast mode)
- invitation-processor (exclusive mode)
info: EventConsumerBackgroundService[0]
Event consumer starting...
info: EventConsumerBackgroundService[0]
Subscribing to 'user-analytics' subscription (broadcast mode)...
✅ Verify: Application starts without errors and background consumer logs appear.
Test 1: Workflow Start Semantics
Objective: Verify that commands create workflow instances with correlation IDs.
Test 1.1: Add User Command (HTTP)
curl -X POST http://localhost:6001/api/command/addUser \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com"
}'
Expected Response:
5432
(Returns the generated user ID)
Expected Console Output:
info: EventConsumerBackgroundService[0]
[ANALYTICS] Received event: UserAddedEvent (EventId: <guid>, CorrelationId: <workflow-id>, OccurredAt: <timestamp>)
info: EventConsumerBackgroundService[0]
[ANALYTICS] User added: UserId=5432, Name=John Doe
✅ Verify:
- Command returns user ID
- EventConsumerBackgroundService logs show event received
- CorrelationId is present and is a GUID (workflow ID)
Test 1.2: Invite User Command (Multi-Step Workflow)
Step 1: Send Invitation
curl -X POST http://localhost:6001/api/command/inviteUser \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"inviterName": "Admin"
}'
Expected Response:
"<invitation-id-guid>"
Expected Console Output:
info: EventConsumerBackgroundService[0]
[ANALYTICS] Received event: UserInvitedEvent (EventId: <guid>, CorrelationId: <workflow-id>, ...)
info: EventConsumerBackgroundService[0]
[ANALYTICS] User invited: InvitationId=<invitation-id>, Email=jane@example.com, Inviter=Admin
Step 2: Accept Invitation
curl -X POST http://localhost:6001/api/command/acceptInvite \
-H "Content-Type: application/json" \
-d '{
"invitationId": "<invitation-id-from-step-1>",
"email": "jane@example.com",
"name": "Jane Doe"
}'
Expected Response:
7891
(Returns the generated user ID)
Expected Console Output:
info: EventConsumerBackgroundService[0]
[ANALYTICS] Received event: UserInviteAcceptedEvent (EventId: <guid>, CorrelationId: <new-workflow-id>, ...)
info: EventConsumerBackgroundService[0]
[ANALYTICS] Invitation accepted: InvitationId=<invitation-id>, UserId=7891, Name=Jane Doe
✅ Verify:
- Both commands complete successfully
- Events are emitted for both steps
- Each command creates its own workflow instance (Phase 1 behavior)
- Different CorrelationIds for invite vs accept (Phase 1 limitation - Phase 2 will support continuation)
Test 2: Event Consumer (Broadcast Mode)
Objective: Verify that broadcast subscription delivers events to all consumers.
Test 2.1: Multiple Events
Execute multiple commands and observe that EventConsumerBackgroundService receives all events:
# Add multiple users
for i in {1..5}; do
curl -X POST http://localhost:6001/api/command/addUser \
-H "Content-Type: application/json" \
-d "{\"name\": \"User $i\", \"email\": \"user$i@example.com\"}"
sleep 1
done
Expected Console Output:
info: EventConsumerBackgroundService[0]
[ANALYTICS] Received event: UserAddedEvent (EventId: <guid-1>, ...)
info: EventConsumerBackgroundService[0]
[ANALYTICS] User added: UserId=<id-1>, Name=User 1
info: EventConsumerBackgroundService[0]
[ANALYTICS] Received event: UserAddedEvent (EventId: <guid-2>, ...)
info: EventConsumerBackgroundService[0]
[ANALYTICS] User added: UserId=<id-2>, Name=User 2
...
✅ Verify:
- All 5 events are received by the consumer
- Events appear in order
- No events are missed (broadcast mode guarantees all consumers get all events)
Test 3: Ephemeral Streams
Objective: Verify ephemeral stream behavior (message queue semantics).
Test 3.1: Event Visibility Timeout
Ephemeral streams use visibility timeouts. Events that aren't acknowledged within the timeout are automatically requeued.
Current Behavior (Phase 1.4):
- EventSubscriptionClient automatically acknowledges events after processing
- Visibility timeout is set to 30 seconds by default
- Events are deleted after acknowledgment (ephemeral semantics)
Manual Test:
- Send a command to generate an event
- Observe that the event is delivered to the consumer
- Event is automatically acknowledged and removed from the stream
curl -X POST http://localhost:6001/api/command/addUser \
-H "Content-Type: application/json" \
-d '{"name": "Test User", "email": "test@example.com"}'
✅ Verify:
- Event is delivered once to the consumer
- No duplicate deliveries (event is removed after acknowledgment)
- If you stop and restart the app, previous events are gone (ephemeral semantics)
Test 3.2: Application Restart (Ephemeral Behavior)
- Send several commands to generate events
- Stop the application (Ctrl+C)
- Restart the application
- Observe that previous events are NOT replayed (ephemeral streams don't persist data)
# While app is running
curl -X POST http://localhost:6001/api/command/addUser \
-H "Content-Type: application/json" \
-d '{"name": "Before Restart", "email": "before@example.com"}'
# Stop app (Ctrl+C)
# Restart app
# No events from before restart are delivered
✅ Verify:
- After restart, no historical events are delivered
- Only new events (after restart) are received
- This confirms ephemeral stream behavior (data is not persisted)
Test 4: gRPC Streaming
Objective: Verify gRPC EventService bidirectional streaming.
Test 4.1: List gRPC Services
grpcurl -plaintext localhost:6000 list
Expected Output:
grpc.reflection.v1.ServerReflection
grpc.reflection.v1alpha.ServerReflection
svrnty.cqrs.CommandService
svrnty.cqrs.DynamicQueryService
svrnty.cqrs.QueryService
svrnty.cqrs.events.EventService
✅ Verify: svrnty.cqrs.events.EventService is listed
Test 4.2: Inspect EventService
grpcurl -plaintext localhost:6000 describe svrnty.cqrs.events.EventService
Expected Output:
svrnty.cqrs.events.EventService is a service:
service EventService {
rpc Subscribe ( stream .svrnty.cqrs.events.SubscriptionRequest ) returns ( stream .svrnty.cqrs.events.EventMessage );
}
✅ Verify: Subscribe method is available with bidirectional streaming
Test 4.3: Subscribe to Events via gRPC
This test requires a separate terminal window for the gRPC client:
Terminal 1: Start gRPC subscription (leave this running)
grpcurl -plaintext -d @ localhost:6000 svrnty.cqrs.events.EventService.Subscribe <<EOF
{
"subscribe": {
"subscription_id": "test-grpc-sub",
"correlation_id": "test-correlation",
"delivery_mode": "DELIVERY_MODE_IMMEDIATE"
}
}
EOF
Terminal 2: Send commands to generate events
curl -X POST http://localhost:6001/api/command/addUser \
-H "Content-Type: application/json" \
-d '{"name": "gRPC Test User", "email": "grpc@example.com"}'
Expected Output in Terminal 1:
{
"event": {
"subscriptionId": "test-grpc-sub",
"correlationId": "<workflow-id>",
"eventType": "UserAddedEvent",
"eventId": "<event-id>",
"sequence": 1,
"occurredAt": "2025-12-09T...",
"placeholder": {
"data": "Event data placeholder"
}
}
}
✅ Verify:
- gRPC client receives event in real-time
- Event contains correct metadata (eventType, correlationId, etc.)
- Phase 1 uses placeholder for event data (Phase 2 will add full event payloads)
Test 4.4: gRPC with Event Type Filtering
grpcurl -plaintext -d @ localhost:6000 svrnty.cqrs.events.EventService.Subscribe <<EOF
{
"subscribe": {
"subscription_id": "filtered-sub",
"correlation_id": "test-correlation",
"delivery_mode": "DELIVERY_MODE_IMMEDIATE",
"event_types": ["UserInvitedEvent", "UserInviteAcceptedEvent"]
}
}
EOF
Send various commands and observe that only UserInvitedEvent and UserInviteAcceptedEvent are delivered.
✅ Verify:
- Only events matching the filter are delivered
- UserAddedEvent and other types are not delivered
Test 5: Existing Features (Regression)
Objective: Verify that existing CQRS functionality still works correctly.
Test 5.1: HTTP Query Endpoint
curl -X GET "http://localhost:6001/api/query/fetchUser?userId=1234"
Expected Response:
{
"userId": 1234,
"name": "John Doe",
"email": "john@example.com"
}
✅ Verify: Query endpoints work correctly
Test 5.2: gRPC Command Service
grpcurl -plaintext -d '{
"addUserCommand": {
"name": "gRPC User",
"email": "grpc-user@example.com"
}
}' localhost:6000 svrnty.cqrs.CommandService.Execute
Expected Response:
{
"addUserCommandResponse": {
"result": 6789
}
}
✅ Verify: gRPC commands still work correctly
Test 5.3: FluentValidation
# Send invalid command (missing required fields)
curl -X POST http://localhost:6001/api/command/addUser \
-H "Content-Type: application/json" \
-d '{}'
Expected Response: HTTP 400 with validation errors
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Name": ["'Name' must not be empty."],
"Email": ["'Email' must not be empty."]
}
}
✅ Verify: Validation still works correctly
Test 5.4: Swagger UI
Navigate to: http://localhost:6001/swagger
✅ Verify:
- Swagger UI loads correctly
- All command and query endpoints are listed
- Can test endpoints interactively
Expected Results Summary
✅ Phase 1.1 - Workflow Abstraction
- Commands create workflow instances with unique IDs
- Events are emitted with workflow ID as correlation ID
- Multi-step workflows work (invite → accept/decline)
✅ Phase 1.2 - Stream Configuration
- Streams can be configured with fluent API
- Ephemeral stream type is supported
- At-least-once delivery semantics work
✅ Phase 1.3 - In-Memory Storage
- InMemoryEventStreamStore handles ephemeral operations
- Events are enqueued and dequeued correctly
- Consumer registry tracks active consumers
✅ Phase 1.4 - Subscription System
- IEventSubscriptionClient provides async enumerable interface
- Broadcast mode delivers events to all consumers
- Exclusive mode would distribute load (demonstrated by single consumer)
- Automatic acknowledgment works
- Visibility timeout enforcement works
✅ Phase 1.7 - gRPC Streaming
- EventService is available via gRPC
- Bidirectional streaming works
- Event type filtering works
- Acknowledge/Nack commands are accepted (logged)
- GrpcEventDeliveryProvider is registered and operational
✅ Phase 1.8 - Sample Project
- Sample project demonstrates all features
- EventConsumerBackgroundService shows event consumption
- Documentation provides usage examples
- Application starts and runs correctly
✅ Regression Tests
- Existing HTTP endpoints work
- Existing gRPC endpoints work
- FluentValidation works
- Swagger UI works
- Dynamic queries work
Known Limitations (Phase 1)
These are expected limitations that will be addressed in future phases:
-
No Workflow Continuation: Each command creates a new workflow instance. Multi-step workflows (invite → accept) have different correlation IDs.
- Phase 2 Fix: Workflow continuation API will allow referencing existing workflow instances
-
Placeholder Event Data: gRPC events use placeholder data instead of actual event payloads.
- Phase 2 Fix: Source generator will add strongly-typed event messages to proto file
-
Polling-Based Delivery: EventSubscriptionClient uses polling (100ms intervals) instead of true push.
- Phase 2 Fix: Channel-based push delivery will eliminate polling
-
No Persistent Streams: Only ephemeral streams are supported. Data is lost on restart.
- Phase 2 Fix: Persistent streams with event sourcing capabilities
-
Manual Ack/Nack Not Functional: Acknowledge and Nack commands are logged but don't affect delivery.
- Phase 2 Fix: Full manual acknowledgment support with retry logic
Troubleshooting
Issue: "Address already in use" errors
Solution: Kill any processes using ports 6000 or 6001:
# macOS/Linux
lsof -ti:6000 | xargs kill -9
lsof -ti:6001 | xargs kill -9
# Windows
netstat -ano | findstr :6000
taskkill /PID <pid> /F
Issue: Event consumer not logging events
Check:
- Application started successfully
- EventConsumerBackgroundService is registered in Program.cs
- Subscription "user-analytics" matches configured subscription ID
- Check application logs for errors
Issue: grpcurl not found
Solution:
# macOS
brew install grpcurl
# Linux
wget https://github.com/fullstorydev/grpcurl/releases/download/v1.8.9/grpcurl_1.8.9_linux_x86_64.tar.gz
tar -xvf grpcurl_1.8.9_linux_x86_64.tar.gz
sudo mv grpcurl /usr/local/bin/
# Windows
choco install grpcurl
Next Steps
After completing Phase 1 testing:
-
Phase 2: Persistent Streams & Event Sourcing
- Add EventStoreDB or similar persistent storage
- Implement stream replay capabilities
- Add snapshot support
-
Phase 3: Advanced Features
- Consumer groups (Kafka-style partitioning)
- Dead letter queues
- Retry policies
- Circuit breakers
-
Production Readiness
- Add comprehensive unit tests
- Add integration tests
- Performance benchmarking
- Monitoring and observability