flow_chat 0.6.0 → 0.7.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +44 -0
  3. data/.gitignore +2 -1
  4. data/README.md +84 -1229
  5. data/docs/configuration.md +337 -0
  6. data/docs/flows.md +320 -0
  7. data/docs/images/simulator.png +0 -0
  8. data/docs/instrumentation.md +216 -0
  9. data/docs/media.md +153 -0
  10. data/docs/testing.md +475 -0
  11. data/docs/ussd-setup.md +306 -0
  12. data/docs/whatsapp-setup.md +162 -0
  13. data/examples/multi_tenant_whatsapp_controller.rb +9 -37
  14. data/examples/simulator_controller.rb +9 -18
  15. data/examples/ussd_controller.rb +32 -38
  16. data/examples/whatsapp_controller.rb +32 -125
  17. data/examples/whatsapp_media_examples.rb +68 -336
  18. data/examples/whatsapp_message_job.rb +5 -3
  19. data/flow_chat.gemspec +6 -2
  20. data/lib/flow_chat/base_processor.rb +48 -2
  21. data/lib/flow_chat/config.rb +5 -0
  22. data/lib/flow_chat/context.rb +13 -1
  23. data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
  24. data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
  25. data/lib/flow_chat/instrumentation/setup.rb +155 -0
  26. data/lib/flow_chat/instrumentation.rb +70 -0
  27. data/lib/flow_chat/prompt.rb +20 -20
  28. data/lib/flow_chat/session/cache_session_store.rb +73 -7
  29. data/lib/flow_chat/session/middleware.rb +37 -4
  30. data/lib/flow_chat/session/rails_session_store.rb +36 -1
  31. data/lib/flow_chat/simulator/controller.rb +7 -7
  32. data/lib/flow_chat/ussd/app.rb +1 -1
  33. data/lib/flow_chat/ussd/gateway/nalo.rb +30 -0
  34. data/lib/flow_chat/ussd/gateway/nsano.rb +33 -0
  35. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
  36. data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
  37. data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
  38. data/lib/flow_chat/ussd/processor.rb +14 -0
  39. data/lib/flow_chat/ussd/renderer.rb +1 -1
  40. data/lib/flow_chat/version.rb +1 -1
  41. data/lib/flow_chat/whatsapp/app.rb +1 -1
  42. data/lib/flow_chat/whatsapp/client.rb +99 -12
  43. data/lib/flow_chat/whatsapp/configuration.rb +35 -4
  44. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +128 -54
  45. data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
  46. data/lib/flow_chat/whatsapp/processor.rb +8 -0
  47. data/lib/flow_chat/whatsapp/renderer.rb +4 -9
  48. data/lib/flow_chat.rb +23 -0
  49. metadata +22 -11
  50. data/.travis.yml +0 -6
  51. data/app/controllers/demo_controller.rb +0 -101
  52. data/app/flow_chat/demo_restaurant_flow.rb +0 -889
  53. data/config/routes_demo.rb +0 -59
  54. data/examples/initializer.rb +0 -86
  55. data/examples/media_prompts_examples.rb +0 -27
  56. data/images/ussd_simulator.png +0 -0
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
+ provider: "nalo",
47
+ endpoint: "/ussd",
48
+ color: "#007bff"
49
+ },
50
+ whatsapp: {
51
+ name: "WhatsApp Integration",
52
+ icon: "💬",
53
+ processor_type: "whatsapp",
54
+ provider: "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
+ ```