flow_chat 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +44 -0
- data/.gitignore +2 -1
- data/README.md +85 -1229
- data/docs/configuration.md +360 -0
- data/docs/flows.md +320 -0
- data/docs/images/simulator.png +0 -0
- data/docs/instrumentation.md +216 -0
- data/docs/media.md +153 -0
- data/docs/sessions.md +433 -0
- data/docs/testing.md +475 -0
- data/docs/ussd-setup.md +322 -0
- data/docs/whatsapp-setup.md +162 -0
- data/examples/multi_tenant_whatsapp_controller.rb +9 -37
- data/examples/simulator_controller.rb +13 -22
- data/examples/ussd_controller.rb +41 -41
- data/examples/whatsapp_controller.rb +32 -125
- data/examples/whatsapp_media_examples.rb +68 -336
- data/examples/whatsapp_message_job.rb +5 -3
- data/flow_chat.gemspec +6 -2
- data/lib/flow_chat/base_processor.rb +79 -2
- data/lib/flow_chat/config.rb +31 -5
- data/lib/flow_chat/context.rb +13 -1
- data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
- data/lib/flow_chat/instrumentation/setup.rb +155 -0
- data/lib/flow_chat/instrumentation.rb +70 -0
- data/lib/flow_chat/prompt.rb +20 -20
- data/lib/flow_chat/session/cache_session_store.rb +73 -7
- data/lib/flow_chat/session/middleware.rb +130 -12
- data/lib/flow_chat/session/rails_session_store.rb +36 -1
- data/lib/flow_chat/simulator/controller.rb +8 -8
- data/lib/flow_chat/simulator/views/simulator.html.erb +5 -5
- data/lib/flow_chat/ussd/gateway/nalo.rb +31 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +36 -2
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
- data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
- data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
- data/lib/flow_chat/ussd/processor.rb +16 -4
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +99 -12
- data/lib/flow_chat/whatsapp/configuration.rb +35 -4
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +121 -34
- data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
- data/lib/flow_chat/whatsapp/processor.rb +7 -1
- data/lib/flow_chat/whatsapp/renderer.rb +4 -9
- data/lib/flow_chat.rb +23 -0
- metadata +23 -12
- data/.travis.yml +0 -6
- data/app/controllers/demo_controller.rb +0 -101
- data/app/flow_chat/demo_restaurant_flow.rb +0 -889
- data/config/routes_demo.rb +0 -59
- data/examples/initializer.rb +0 -86
- data/examples/media_prompts_examples.rb +0 -27
- data/images/ussd_simulator.png +0 -0
- data/lib/flow_chat/ussd/middleware/resumable_session.rb +0 -39
data/docs/testing.md
ADDED
@@ -0,0 +1,475 @@
|
|
1
|
+
# Testing Guide
|
2
|
+
|
3
|
+
FlowChat provides comprehensive testing capabilities for both USSD and WhatsApp flows. This guide covers everything from unit testing individual flows to using the powerful built-in simulator for interactive testing.
|
4
|
+
|
5
|
+
## Testing Approaches
|
6
|
+
|
7
|
+
FlowChat supports multiple testing strategies depending on your needs:
|
8
|
+
|
9
|
+
| Approach | Best For | Setup Complexity | Real API Calls |
|
10
|
+
|----------|----------|------------------|----------------|
|
11
|
+
| **Unit Testing** | Individual flow logic | Low | No |
|
12
|
+
| **Simulator Mode** | Integration testing, development | Medium | No |
|
13
|
+
| **Skip Validation** | Staging environments | Medium | Yes |
|
14
|
+
| **Full Integration** | Production-like testing | High | Yes |
|
15
|
+
|
16
|
+
## Quick Start: Interactive Simulator
|
17
|
+
|
18
|
+
The fastest way to test your flows is with the built-in web simulator:
|
19
|
+
|
20
|
+
### 1. Configure Simulator
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
# config/initializers/flowchat.rb
|
24
|
+
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_simulator"
|
25
|
+
```
|
26
|
+
|
27
|
+
### 2. Create Simulator Controller
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
# app/controllers/simulator_controller.rb
|
31
|
+
class SimulatorController < ApplicationController
|
32
|
+
include FlowChat::Simulator::Controller
|
33
|
+
|
34
|
+
def index
|
35
|
+
flowchat_simulator
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def configurations
|
41
|
+
{
|
42
|
+
ussd: {
|
43
|
+
name: "USSD Integration",
|
44
|
+
icon: "📱",
|
45
|
+
processor_type: "ussd",
|
46
|
+
gateway: "nalo",
|
47
|
+
endpoint: "/ussd",
|
48
|
+
color: "#007bff"
|
49
|
+
},
|
50
|
+
whatsapp: {
|
51
|
+
name: "WhatsApp Integration",
|
52
|
+
icon: "💬",
|
53
|
+
processor_type: "whatsapp",
|
54
|
+
gateway: "cloud_api",
|
55
|
+
endpoint: "/whatsapp/webhook",
|
56
|
+
color: "#25D366"
|
57
|
+
}
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def default_config_key
|
62
|
+
:whatsapp
|
63
|
+
end
|
64
|
+
|
65
|
+
def default_phone_number
|
66
|
+
"+1234567890"
|
67
|
+
end
|
68
|
+
|
69
|
+
def default_contact_name
|
70
|
+
"Test User"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
### 3. Add Route
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
# config/routes.rb
|
79
|
+
Rails.application.routes.draw do
|
80
|
+
get '/simulator' => 'simulator#index'
|
81
|
+
# ... your other routes
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
### 4. Enable Simulator in Controllers
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
# app/controllers/whatsapp_controller.rb
|
89
|
+
class WhatsappController < ApplicationController
|
90
|
+
skip_forgery_protection
|
91
|
+
|
92
|
+
def webhook
|
93
|
+
enable_simulator = Rails.env.development? # enabled in development by default
|
94
|
+
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator:) do |config|
|
95
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
96
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
97
|
+
end
|
98
|
+
|
99
|
+
processor.run WelcomeFlow, :main_page
|
100
|
+
end
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
### 5. Test Your Flows
|
105
|
+
|
106
|
+
Visit [http://localhost:3000/simulator](http://localhost:3000/simulator) and start testing!
|
107
|
+
|
108
|
+
**Simulator Features:**
|
109
|
+
- 📱 **Visual Interface** - Phone-like display showing actual conversation
|
110
|
+
- 🔄 **Platform Switching** - Toggle between USSD and WhatsApp modes
|
111
|
+
- 📊 **Request Logging** - See HTTP requests and responses in real-time
|
112
|
+
- 🎯 **Interactive Testing** - Character counting, validation, session management
|
113
|
+
- 🛠️ **Developer Tools** - Reset sessions, view connection status
|
114
|
+
|
115
|
+
## Unit Testing
|
116
|
+
|
117
|
+
Test individual flows in isolation:
|
118
|
+
|
119
|
+
### Basic Flow Testing
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
# test/flows/welcome_flow_test.rb
|
123
|
+
require 'test_helper'
|
124
|
+
|
125
|
+
class WelcomeFlowTest < ActiveSupport::TestCase
|
126
|
+
def setup
|
127
|
+
@context = FlowChat::Context.new
|
128
|
+
@context.session = FlowChat::Session::CacheSessionStore.new
|
129
|
+
@context.session.init_session("test_session")
|
130
|
+
end
|
131
|
+
|
132
|
+
test "welcome flow collects name and shows greeting" do
|
133
|
+
# Simulate user entering name
|
134
|
+
@context.input = "John Doe"
|
135
|
+
app = FlowChat::Ussd::App.new(@context)
|
136
|
+
|
137
|
+
# Expect flow to terminate with greeting
|
138
|
+
error = assert_raises(FlowChat::Interrupt::Terminate) do
|
139
|
+
flow = WelcomeFlow.new(app)
|
140
|
+
flow.main_page
|
141
|
+
end
|
142
|
+
|
143
|
+
assert_includes error.prompt, "Hello, John Doe"
|
144
|
+
end
|
145
|
+
|
146
|
+
test "flow handles validation errors" do
|
147
|
+
# Test with empty input
|
148
|
+
@context.input = ""
|
149
|
+
app = FlowChat::Ussd::App.new(@context)
|
150
|
+
|
151
|
+
# Should prompt for input again
|
152
|
+
error = assert_raises(FlowChat::Interrupt::Input) do
|
153
|
+
flow = RegistrationFlow.new(app)
|
154
|
+
flow.collect_email
|
155
|
+
end
|
156
|
+
|
157
|
+
assert_includes error.prompt, "Email is required"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
### Testing Complex Flows
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
# test/flows/registration_flow_test.rb
|
166
|
+
class RegistrationFlowTest < ActiveSupport::TestCase
|
167
|
+
test "complete registration flow" do
|
168
|
+
context = FlowChat::Context.new
|
169
|
+
context.session = FlowChat::Session::CacheSessionStore.new
|
170
|
+
context.session.init_session("test_session")
|
171
|
+
|
172
|
+
# Step 1: Enter email
|
173
|
+
context.input = "john@example.com"
|
174
|
+
app = FlowChat::Ussd::App.new(context)
|
175
|
+
|
176
|
+
assert_raises(FlowChat::Interrupt::Input) do
|
177
|
+
flow = RegistrationFlow.new(app)
|
178
|
+
flow.main_page
|
179
|
+
end
|
180
|
+
|
181
|
+
# Verify email was stored
|
182
|
+
assert_equal "john@example.com", context.session.get(:email)
|
183
|
+
|
184
|
+
# Step 2: Enter age
|
185
|
+
context.input = "25"
|
186
|
+
|
187
|
+
assert_raises(FlowChat::Interrupt::Input) do
|
188
|
+
flow = RegistrationFlow.new(app)
|
189
|
+
flow.main_page # Continue from where we left off
|
190
|
+
end
|
191
|
+
|
192
|
+
# Step 3: Confirm
|
193
|
+
context.input = "yes"
|
194
|
+
|
195
|
+
assert_raises(FlowChat::Interrupt::Terminate) do
|
196
|
+
flow = RegistrationFlow.new(app)
|
197
|
+
flow.main_page
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
## Integration Testing
|
204
|
+
|
205
|
+
### Environment Configuration
|
206
|
+
|
207
|
+
Set up different testing modes per environment:
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
# config/initializers/flowchat.rb
|
211
|
+
case Rails.env
|
212
|
+
when 'development'
|
213
|
+
# Use simulator for easy testing
|
214
|
+
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
215
|
+
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_dev"
|
216
|
+
|
217
|
+
when 'test'
|
218
|
+
# Use simulator for automated tests
|
219
|
+
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
220
|
+
FlowChat::Config.simulator_secret = "test_secret_key"
|
221
|
+
|
222
|
+
when 'staging'
|
223
|
+
# Use inline mode with real WhatsApp API but skip validation for testing
|
224
|
+
FlowChat::Config.whatsapp.message_handling_mode = :inline
|
225
|
+
FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
|
226
|
+
|
227
|
+
when 'production'
|
228
|
+
# Use background jobs with full security
|
229
|
+
FlowChat::Config.whatsapp.message_handling_mode = :background
|
230
|
+
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
231
|
+
# No simulator secret in production
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
### Simulator Mode Testing
|
236
|
+
|
237
|
+
Test webhook endpoints using simulator mode:
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
# test/controllers/whatsapp_controller_test.rb
|
241
|
+
class WhatsappControllerTest < ActionDispatch::IntegrationTest
|
242
|
+
test "processes whatsapp message in simulator mode" do
|
243
|
+
webhook_payload = {
|
244
|
+
entry: [{
|
245
|
+
changes: [{
|
246
|
+
value: {
|
247
|
+
messages: [{
|
248
|
+
from: "1234567890",
|
249
|
+
text: { body: "Hello" },
|
250
|
+
type: "text",
|
251
|
+
id: "msg_123",
|
252
|
+
timestamp: Time.now.to_i
|
253
|
+
}]
|
254
|
+
}
|
255
|
+
}]
|
256
|
+
}],
|
257
|
+
simulator_mode: true # Enable simulator mode
|
258
|
+
}
|
259
|
+
|
260
|
+
# Generate valid simulator cookie
|
261
|
+
valid_cookie = generate_simulator_cookie
|
262
|
+
|
263
|
+
post "/whatsapp/webhook",
|
264
|
+
params: webhook_payload,
|
265
|
+
cookies: { flowchat_simulator: valid_cookie }
|
266
|
+
|
267
|
+
assert_response :success
|
268
|
+
|
269
|
+
# In simulator mode, response contains message data
|
270
|
+
response_data = JSON.parse(response.body)
|
271
|
+
assert response_data.key?("text")
|
272
|
+
assert_includes response_data["text"], "What's your name?"
|
273
|
+
end
|
274
|
+
|
275
|
+
test "multi-step flow maintains state" do
|
276
|
+
valid_cookie = generate_simulator_cookie
|
277
|
+
|
278
|
+
# Step 1: Start conversation
|
279
|
+
post_simulator_message("start", valid_cookie)
|
280
|
+
assert_response :success
|
281
|
+
|
282
|
+
# Step 2: Enter name
|
283
|
+
post_simulator_message("John", valid_cookie)
|
284
|
+
assert_response :success
|
285
|
+
|
286
|
+
response_data = JSON.parse(response.body)
|
287
|
+
assert_includes response_data["text"], "Hello John"
|
288
|
+
end
|
289
|
+
|
290
|
+
private
|
291
|
+
|
292
|
+
def generate_simulator_cookie(secret = "test_secret_key")
|
293
|
+
timestamp = Time.now.to_i
|
294
|
+
message = "simulator:#{timestamp}"
|
295
|
+
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, message)
|
296
|
+
"#{timestamp}:#{signature}"
|
297
|
+
end
|
298
|
+
|
299
|
+
def post_simulator_message(text, cookie)
|
300
|
+
webhook_payload = {
|
301
|
+
entry: [{
|
302
|
+
changes: [{
|
303
|
+
value: {
|
304
|
+
messages: [{
|
305
|
+
from: "1234567890",
|
306
|
+
text: { body: text },
|
307
|
+
type: "text",
|
308
|
+
id: "msg_#{rand(1000)}",
|
309
|
+
timestamp: Time.now.to_i
|
310
|
+
}]
|
311
|
+
}
|
312
|
+
}]
|
313
|
+
}],
|
314
|
+
simulator_mode: true
|
315
|
+
}
|
316
|
+
|
317
|
+
post "/whatsapp/webhook",
|
318
|
+
params: webhook_payload,
|
319
|
+
cookies: { flowchat_simulator: cookie }
|
320
|
+
end
|
321
|
+
end
|
322
|
+
```
|
323
|
+
|
324
|
+
### Testing with Disabled Validation
|
325
|
+
|
326
|
+
For staging environments where you want to test real endpoints:
|
327
|
+
|
328
|
+
```ruby
|
329
|
+
test "webhook with disabled validation" do
|
330
|
+
# Create config with validation disabled
|
331
|
+
config = FlowChat::Whatsapp::Configuration.new(:test_config)
|
332
|
+
config.access_token = "test_token"
|
333
|
+
config.phone_number_id = "test_phone_id"
|
334
|
+
config.verify_token = "test_verify"
|
335
|
+
config.skip_signature_validation = true # Disable validation for testing
|
336
|
+
|
337
|
+
webhook_payload = {
|
338
|
+
entry: [{
|
339
|
+
changes: [{
|
340
|
+
value: {
|
341
|
+
messages: [{
|
342
|
+
from: "1234567890",
|
343
|
+
text: { body: "Hello" },
|
344
|
+
type: "text"
|
345
|
+
}]
|
346
|
+
}
|
347
|
+
}]
|
348
|
+
}]
|
349
|
+
}
|
350
|
+
|
351
|
+
post "/whatsapp/webhook",
|
352
|
+
params: webhook_payload.to_json,
|
353
|
+
headers: { "Content-Type" => "application/json" }
|
354
|
+
|
355
|
+
assert_response :success
|
356
|
+
end
|
357
|
+
```
|
358
|
+
|
359
|
+
## Advanced Testing Scenarios
|
360
|
+
|
361
|
+
### Testing Error Handling
|
362
|
+
|
363
|
+
```ruby
|
364
|
+
test "handles validation errors gracefully" do
|
365
|
+
valid_cookie = generate_simulator_cookie
|
366
|
+
|
367
|
+
# Send invalid email
|
368
|
+
post_simulator_message("invalid-email", valid_cookie)
|
369
|
+
|
370
|
+
response_data = JSON.parse(response.body)
|
371
|
+
assert_includes response_data["text"], "Invalid email format"
|
372
|
+
|
373
|
+
# Send valid email - should proceed
|
374
|
+
post_simulator_message("john@example.com", valid_cookie)
|
375
|
+
|
376
|
+
response_data = JSON.parse(response.body)
|
377
|
+
refute_includes response_data["text"], "Invalid email"
|
378
|
+
end
|
379
|
+
```
|
380
|
+
|
381
|
+
### Testing Media Responses
|
382
|
+
|
383
|
+
```ruby
|
384
|
+
test "media responses in simulator mode" do
|
385
|
+
valid_cookie = generate_simulator_cookie
|
386
|
+
|
387
|
+
post_simulator_message("help", valid_cookie)
|
388
|
+
|
389
|
+
response_data = JSON.parse(response.body)
|
390
|
+
|
391
|
+
# Check media is included
|
392
|
+
assert response_data.key?("media")
|
393
|
+
assert_equal "image", response_data["media"]["type"]
|
394
|
+
assert response_data["media"]["url"].present?
|
395
|
+
end
|
396
|
+
```
|
397
|
+
|
398
|
+
### Testing Session Persistence
|
399
|
+
|
400
|
+
```ruby
|
401
|
+
test "session data persists across requests" do
|
402
|
+
valid_cookie = generate_simulator_cookie
|
403
|
+
|
404
|
+
# First request - enter name
|
405
|
+
post_simulator_message("John", valid_cookie)
|
406
|
+
|
407
|
+
# Second request - session should remember name
|
408
|
+
post_simulator_message("continue", valid_cookie)
|
409
|
+
|
410
|
+
response_data = JSON.parse(response.body)
|
411
|
+
assert_includes response_data["text"], "John" # Name should be remembered
|
412
|
+
end
|
413
|
+
```
|
414
|
+
|
415
|
+
## Performance Testing
|
416
|
+
|
417
|
+
### Load Testing Background Jobs
|
418
|
+
|
419
|
+
```ruby
|
420
|
+
test "handles high message volume with background jobs" do
|
421
|
+
# Switch to background mode for this test
|
422
|
+
original_mode = FlowChat::Config.whatsapp.message_handling_mode
|
423
|
+
FlowChat::Config.whatsapp.message_handling_mode = :background
|
424
|
+
|
425
|
+
messages = 10.times.map do |i|
|
426
|
+
create_whatsapp_message_payload("user#{i}")
|
427
|
+
end
|
428
|
+
|
429
|
+
assert_enqueued_jobs 10 do
|
430
|
+
messages.each do |msg|
|
431
|
+
post "/whatsapp/webhook", params: msg
|
432
|
+
end
|
433
|
+
end
|
434
|
+
ensure
|
435
|
+
FlowChat::Config.whatsapp.message_handling_mode = original_mode
|
436
|
+
end
|
437
|
+
```
|
438
|
+
|
439
|
+
## Debugging Tests
|
440
|
+
|
441
|
+
### Enable Debug Logging
|
442
|
+
|
443
|
+
```ruby
|
444
|
+
# config/environments/test.rb
|
445
|
+
config.log_level = :debug
|
446
|
+
|
447
|
+
# In tests
|
448
|
+
Rails.logger.debug "Current session data: #{context.session.data}"
|
449
|
+
```
|
450
|
+
|
451
|
+
### Inspect Flow State
|
452
|
+
|
453
|
+
```ruby
|
454
|
+
test "debug flow execution" do
|
455
|
+
context = FlowChat::Context.new
|
456
|
+
context.session = FlowChat::Session::CacheSessionStore.new
|
457
|
+
context.session.init_session("debug_session")
|
458
|
+
|
459
|
+
# Add debugging
|
460
|
+
context.input = "test@example.com"
|
461
|
+
app = FlowChat::Ussd::App.new(context)
|
462
|
+
|
463
|
+
flow = RegistrationFlow.new(app)
|
464
|
+
|
465
|
+
# Inspect state before execution
|
466
|
+
puts "Session before: #{context.session.data}"
|
467
|
+
|
468
|
+
begin
|
469
|
+
flow.main_page
|
470
|
+
rescue FlowChat::Interrupt::Input => e
|
471
|
+
puts "Flow interrupted with: #{e.prompt}"
|
472
|
+
puts "Session after: #{context.session.data}"
|
473
|
+
end
|
474
|
+
end
|
475
|
+
```
|