flow_chat 0.8.2 → 0.10.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/.cliff.toml +74 -0
- data/.github/workflows/ci.yml +2 -3
- data/.github/workflows/pages.yml +43 -0
- data/.github/workflows/release.yml +56 -0
- data/.standard.yml +4 -0
- data/CHANGELOG.md +48 -0
- data/CLAUDE.md +327 -0
- data/CONTRIBUTING.md +134 -0
- data/Gemfile +1 -0
- data/README.md +189 -133
- data/Rakefile +17 -2
- data/SECURITY.md +42 -349
- data/docs/architecture.md +83 -0
- data/docs/async-background-processing.md +64 -0
- data/docs/configuration.md +110 -287
- data/docs/factory-pattern.md +58 -0
- data/docs/gateway-context-variables.md +168 -0
- data/docs/gateway-development.md +159 -0
- data/docs/getting-started.md +90 -0
- data/docs/instrumentation.md +95 -175
- data/docs/platforms/instagram.md +278 -0
- data/docs/platforms/messenger.md +205 -0
- data/docs/platforms/telegram.md +109 -0
- data/docs/platforms/ussd.md +78 -0
- data/docs/platforms/whatsapp.md +147 -0
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
- data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
- data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
- data/docs/testing.md +33 -426
- data/examples/custom_session_id_example.rb +119 -0
- data/examples/http_controller.rb +22 -20
- data/examples/intercom_configuration_example.rb +113 -0
- data/examples/intercom_controller.rb +182 -0
- data/examples/multi_tenant_whatsapp_controller.rb +63 -168
- data/examples/simulator_controller.rb +0 -1
- data/examples/ussd_controller.rb +88 -160
- data/examples/whatsapp_controller.rb +18 -17
- data/examples/whatsapp_media_examples.rb +27 -79
- data/flow_chat.gemspec +4 -0
- data/lib/flow_chat/app.rb +211 -0
- data/lib/flow_chat/async_job.rb +176 -0
- data/lib/flow_chat/choice_titles.rb +95 -0
- data/lib/flow_chat/config.rb +126 -23
- data/lib/flow_chat/delivery_error.rb +9 -0
- data/lib/flow_chat/{base_executor.rb → executor.rb} +6 -11
- data/lib/flow_chat/factory.rb +94 -0
- data/lib/flow_chat/gateway_async_support.rb +106 -0
- data/lib/flow_chat/generic_async_job.rb +30 -0
- data/lib/flow_chat/http/configuration_error.rb +9 -0
- data/lib/flow_chat/http/gateway/simple.rb +104 -36
- data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
- data/lib/flow_chat/http/renderer.rb +3 -3
- data/lib/flow_chat/input.rb +86 -0
- data/lib/flow_chat/instagram/client.rb +32 -0
- data/lib/flow_chat/instagram/configuration.rb +147 -0
- data/lib/flow_chat/instagram/configuration_error.rb +7 -0
- data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
- data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
- data/lib/flow_chat/instagram/renderer.rb +23 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
- data/lib/flow_chat/instrumentation/setup.rb +1 -1
- data/lib/flow_chat/instrumentation.rb +182 -0
- data/lib/flow_chat/intercom/client.rb +161 -0
- data/lib/flow_chat/intercom/configuration.rb +102 -0
- data/lib/flow_chat/intercom/configuration_error.rb +9 -0
- data/lib/flow_chat/intercom/gateway/intercom_api.rb +420 -0
- data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
- data/lib/flow_chat/intercom/renderer.rb +123 -0
- data/lib/flow_chat/media.rb +121 -0
- data/lib/flow_chat/messenger/client.rb +264 -0
- data/lib/flow_chat/messenger/configuration.rb +103 -0
- data/lib/flow_chat/messenger/configuration_error.rb +9 -0
- data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
- data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
- data/lib/flow_chat/messenger/renderer.rb +150 -0
- data/lib/flow_chat/meta/challenge.rb +24 -0
- data/lib/flow_chat/meta/choice_ladder.rb +37 -0
- data/lib/flow_chat/meta/configuration_error.rb +7 -0
- data/lib/flow_chat/meta/gateway_identity.rb +38 -0
- data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
- data/lib/flow_chat/meta/signature.rb +30 -0
- data/lib/flow_chat/meta/signature_validation.rb +66 -0
- data/lib/flow_chat/meta/webhook_verification.rb +43 -0
- data/lib/flow_chat/named_configuration.rb +65 -0
- data/lib/flow_chat/phone_number_util.rb +37 -35
- data/lib/flow_chat/processor.rb +188 -0
- data/lib/flow_chat/prompt.rb +13 -16
- data/lib/flow_chat/renderers/markdown_support.rb +167 -0
- data/lib/flow_chat/security.rb +76 -0
- data/lib/flow_chat/session/middleware.rb +36 -11
- data/lib/flow_chat/simulator/controller.rb +31 -15
- data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
- data/lib/flow_chat/telegram/client.rb +283 -0
- data/lib/flow_chat/telegram/configuration.rb +78 -0
- data/lib/flow_chat/telegram/configuration_error.rb +9 -0
- data/lib/flow_chat/telegram/gateway/bot_api.rb +318 -0
- data/lib/flow_chat/telegram/middleware/choice_mapper.rb +96 -0
- data/lib/flow_chat/telegram/renderer.rb +133 -0
- data/lib/flow_chat/telegram.rb +7 -0
- data/lib/flow_chat/text_truncator.rb +75 -0
- data/lib/flow_chat/ussd/gateway/nalo.rb +24 -4
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
- data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +158 -20
- data/lib/flow_chat/whatsapp/configuration.rb +13 -52
- data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +335 -248
- data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +234 -0
- data/lib/flow_chat/whatsapp/renderer.rb +259 -64
- data/lib/flow_chat.rb +1 -1
- data/lib/tasks/release.rake +165 -0
- data/site/.nojekyll +0 -0
- data/site/.og-card.html +89 -0
- data/site/favicon.svg +6 -0
- data/site/index.html +209 -0
- data/site/og.png +0 -0
- metadata +132 -25
- data/docs/flows.md +0 -320
- data/docs/http-gateway-protocol.md +0 -432
- data/docs/images/simulator.png +0 -0
- data/docs/media.md +0 -153
- data/docs/sessions.md +0 -433
- data/docs/ussd-setup.md +0 -322
- data/docs/whatsapp-setup.md +0 -162
- data/examples/whatsapp_message_job.rb +0 -113
- data/lib/flow_chat/base_app.rb +0 -86
- data/lib/flow_chat/base_processor.rb +0 -146
- data/lib/flow_chat/http/app.rb +0 -6
- data/lib/flow_chat/http/middleware/executor.rb +0 -24
- data/lib/flow_chat/http/processor.rb +0 -33
- data/lib/flow_chat/session/rails_session_store.rb +0 -68
- data/lib/flow_chat/ussd/app.rb +0 -6
- data/lib/flow_chat/ussd/gateway/nsano.rb +0 -96
- data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
- data/lib/flow_chat/ussd/processor.rb +0 -39
- data/lib/flow_chat/whatsapp/app.rb +0 -29
- data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
- data/lib/flow_chat/whatsapp/processor.rb +0 -32
- data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
data/docs/testing.md
CHANGED
|
@@ -1,30 +1,23 @@
|
|
|
1
|
-
# Testing
|
|
1
|
+
# Testing
|
|
2
2
|
|
|
3
|
-
FlowChat
|
|
3
|
+
FlowChat gives you two ways to exercise a flow without a live gateway: a web simulator for manual, interactive testing during development, and the HTTP gateway for automated request tests.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## The simulator
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The simulator is a web page that drives your flows the way a real platform would, so you can step through a conversation from the browser. It works the same regardless of which gateway a flow targets.
|
|
8
8
|
|
|
9
|
-
|
|
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 |
|
|
9
|
+
Enable it in two steps.
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
The fastest way to test your flows is with the built-in web simulator:
|
|
19
|
-
|
|
20
|
-
### 1. Configure Simulator
|
|
11
|
+
Set the simulator secret during boot:
|
|
21
12
|
|
|
22
13
|
```ruby
|
|
23
|
-
# config/initializers/
|
|
24
|
-
FlowChat::Config.simulator_secret = Rails.application.
|
|
14
|
+
# config/initializers/flow_chat.rb
|
|
15
|
+
FlowChat::Config.simulator_secret = Rails.application.credentials.flow_chat_simulator_secret
|
|
25
16
|
```
|
|
26
17
|
|
|
27
|
-
|
|
18
|
+
The secret gates access. The simulator controller signs a cookie with it, and the gateways only enter simulator mode when that cookie is valid, so the simulator stays off in any environment where the secret is unset.
|
|
19
|
+
|
|
20
|
+
Mount a controller that includes the simulator module and lists the endpoints to test:
|
|
28
21
|
|
|
29
22
|
```ruby
|
|
30
23
|
# app/controllers/simulator_controller.rb
|
|
@@ -39,437 +32,51 @@ class SimulatorController < ApplicationController
|
|
|
39
32
|
|
|
40
33
|
def configurations
|
|
41
34
|
{
|
|
42
|
-
|
|
43
|
-
name: "USSD
|
|
44
|
-
icon: "📱",
|
|
35
|
+
ussd_main: {
|
|
36
|
+
name: "USSD",
|
|
45
37
|
processor_type: "ussd",
|
|
46
38
|
gateway: "nalo",
|
|
47
|
-
endpoint: "/ussd"
|
|
48
|
-
color: "#007bff"
|
|
39
|
+
endpoint: "/ussd"
|
|
49
40
|
},
|
|
50
|
-
|
|
51
|
-
name: "WhatsApp
|
|
52
|
-
|
|
53
|
-
processor_type: "whatsapp",
|
|
41
|
+
whatsapp_main: {
|
|
42
|
+
name: "WhatsApp",
|
|
43
|
+
processor_type: "whatsapp",
|
|
54
44
|
gateway: "cloud_api",
|
|
55
|
-
endpoint: "/whatsapp/webhook"
|
|
56
|
-
color: "#25D366"
|
|
45
|
+
endpoint: "/whatsapp/webhook"
|
|
57
46
|
}
|
|
58
47
|
}
|
|
59
48
|
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
49
|
end
|
|
73
50
|
```
|
|
74
51
|
|
|
75
|
-
### 3. Add Route
|
|
76
|
-
|
|
77
52
|
```ruby
|
|
78
53
|
# config/routes.rb
|
|
79
|
-
|
|
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
|
|
54
|
+
get "/simulator", to: "simulator#index"
|
|
379
55
|
```
|
|
380
56
|
|
|
381
|
-
|
|
57
|
+
Each entry in `configurations` points at one of your real webhook endpoints, so the simulator posts to the same controller actions the platform would. Open `/simulator`, pick an endpoint, and send messages.
|
|
382
58
|
|
|
383
|
-
|
|
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
|
-
```
|
|
59
|
+
## Automated tests
|
|
397
60
|
|
|
398
|
-
|
|
61
|
+
For request specs, drive a flow through the HTTP gateway (`FlowChat::Http::Gateway::Simple`), which speaks plain JSON. Point a controller at your flow:
|
|
399
62
|
|
|
400
63
|
```ruby
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
64
|
+
class ChatController < ApplicationController
|
|
65
|
+
def webhook
|
|
66
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
67
|
+
config.use_gateway FlowChat::Http::Gateway::Simple
|
|
68
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
432
69
|
end
|
|
70
|
+
processor.run RegistrationFlow, :main_page
|
|
433
71
|
end
|
|
434
|
-
ensure
|
|
435
|
-
FlowChat::Config.whatsapp.message_handling_mode = original_mode
|
|
436
72
|
end
|
|
437
73
|
```
|
|
438
74
|
|
|
439
|
-
|
|
75
|
+
The Simple gateway expects `session_id` and `user_id` in the request and returns JSON with the prompt, choices, and any media. Post the next input with the same `session_id` to advance the conversation, and assert on the returned message and choices. Set `FlowChat::Config.cache` to a real store (for example `ActiveSupport::Cache::MemoryStore.new`) in your test setup so sessions persist across the requests in a single test.
|
|
440
76
|
|
|
441
|
-
|
|
77
|
+
Because one flow behaves the same across platforms, a flow verified through the HTTP gateway behaves the same on USSD, WhatsApp, and Telegram, apart from each platform's rendering.
|
|
442
78
|
|
|
443
|
-
|
|
444
|
-
# config/environments/test.rb
|
|
445
|
-
config.log_level = :debug
|
|
79
|
+
## Related
|
|
446
80
|
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
```
|
|
81
|
+
- [Getting started](getting-started.md)
|
|
82
|
+
- [Configuration](configuration.md)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example: Custom Session ID Configuration with Proc
|
|
4
|
+
#
|
|
5
|
+
# This example demonstrates how to use a custom proc for session ID generation
|
|
6
|
+
# in FlowChat applications. The proc allows complete customization of how
|
|
7
|
+
# session IDs are generated based on context data.
|
|
8
|
+
|
|
9
|
+
require "flow_chat"
|
|
10
|
+
|
|
11
|
+
class CustomSessionIdController < ApplicationController
|
|
12
|
+
def ussd_endpoint
|
|
13
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
14
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
15
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
16
|
+
|
|
17
|
+
# Example 1: Custom session ID using block/proc
|
|
18
|
+
config.use_session_config do |context|
|
|
19
|
+
# Create a custom session ID based on your business logic
|
|
20
|
+
user_phone = context["request.msisdn"]
|
|
21
|
+
flow_name = context["flow.name"]
|
|
22
|
+
gateway = context["request.gateway"]
|
|
23
|
+
timestamp = Time.current.strftime("%Y%m%d")
|
|
24
|
+
|
|
25
|
+
# Custom format: flow_gateway_date_hashedphone
|
|
26
|
+
"#{flow_name}_#{gateway}_#{timestamp}_#{hash_phone(user_phone)}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
processor.run(SurveyFlow, :main_menu)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def whatsapp_endpoint
|
|
34
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
35
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
36
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
37
|
+
|
|
38
|
+
# Example 2: Multi-tenant session IDs
|
|
39
|
+
config.use_session_config do |context|
|
|
40
|
+
tenant_id = extract_tenant_from_request(context)
|
|
41
|
+
user_id = context["request.user_id"] || context["request.msisdn"]
|
|
42
|
+
flow_name = context["flow.name"]
|
|
43
|
+
|
|
44
|
+
"tenant_#{tenant_id}_flow_#{flow_name}_user_#{hash_identifier(user_id)}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
processor.run(CustomerSupportFlow, :handle_inquiry)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def http_endpoint
|
|
52
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
53
|
+
config.use_gateway FlowChat::Http::Gateway::Simple
|
|
54
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
55
|
+
|
|
56
|
+
# Example 3: API session with custom expiration tracking
|
|
57
|
+
config.use_session_config do |context|
|
|
58
|
+
api_key = context.controller.request.headers["X-API-Key"]
|
|
59
|
+
request_id = context["request.id"]
|
|
60
|
+
|
|
61
|
+
# Include API key hash for session isolation per API client
|
|
62
|
+
"api_#{hash_identifier(api_key)}_req_#{request_id}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
processor.run(ApiFlow, :handle_request)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def hash_phone(phone)
|
|
72
|
+
require "digest"
|
|
73
|
+
Digest::SHA256.hexdigest(phone)[0, 8]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def hash_identifier(identifier)
|
|
77
|
+
require "digest"
|
|
78
|
+
Digest::SHA256.hexdigest(identifier.to_s)[0, 8]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def extract_tenant_from_request(context)
|
|
82
|
+
# Extract tenant from subdomain or header
|
|
83
|
+
request = context.controller&.request
|
|
84
|
+
return "default" unless request
|
|
85
|
+
|
|
86
|
+
host = request.host
|
|
87
|
+
subdomain = host.split(".").first
|
|
88
|
+
subdomain if subdomain != "www"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Example flows for demonstration
|
|
93
|
+
class SurveyFlow < FlowChat::Flow
|
|
94
|
+
def main_menu
|
|
95
|
+
app.screen(:menu) { |p| p.ask "Welcome! Choose an option:", choices: ["Survey", "Exit"] }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class CustomerSupportFlow < FlowChat::Flow
|
|
100
|
+
def handle_inquiry
|
|
101
|
+
app.screen(:inquiry) { |p| p.ask "How can we help you today?" }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class ApiFlow < FlowChat::Flow
|
|
106
|
+
def handle_request
|
|
107
|
+
app.screen(:request) { |p| p.ask "API request received. Provide data:" }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Note: The custom session ID proc approach provides:
|
|
112
|
+
# - Complete control over session ID format
|
|
113
|
+
# - Access to full context (request data, flow info, etc.)
|
|
114
|
+
# - Ability to implement complex business logic
|
|
115
|
+
# - Support for multi-tenancy, API authentication, etc.
|
|
116
|
+
#
|
|
117
|
+
# The proc should return a string that will be used as the session ID.
|
|
118
|
+
# Make sure the returned ID is unique for your use case to avoid
|
|
119
|
+
# session collisions.
|