jetstream_bridge 4.5.0 → 4.5.1

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +338 -87
  3. data/README.md +3 -13
  4. data/docs/GETTING_STARTED.md +8 -12
  5. data/docs/PRODUCTION.md +13 -35
  6. data/docs/RESTRICTED_PERMISSIONS.md +399 -0
  7. data/docs/TESTING.md +33 -22
  8. data/lib/generators/jetstream_bridge/health_check/health_check_generator.rb +3 -3
  9. data/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +3 -0
  10. data/lib/jetstream_bridge/consumer/consumer.rb +100 -39
  11. data/lib/jetstream_bridge/consumer/message_processor.rb +1 -1
  12. data/lib/jetstream_bridge/consumer/subscription_manager.rb +97 -121
  13. data/lib/jetstream_bridge/core/bridge_helpers.rb +127 -0
  14. data/lib/jetstream_bridge/core/config.rb +32 -161
  15. data/lib/jetstream_bridge/core/connection.rb +508 -0
  16. data/lib/jetstream_bridge/core/connection_factory.rb +95 -0
  17. data/lib/jetstream_bridge/core/debug_helper.rb +2 -9
  18. data/lib/jetstream_bridge/core.rb +2 -0
  19. data/lib/jetstream_bridge/models/subject.rb +15 -23
  20. data/lib/jetstream_bridge/provisioner.rb +67 -0
  21. data/lib/jetstream_bridge/publisher/publisher.rb +121 -92
  22. data/lib/jetstream_bridge/rails/integration.rb +5 -8
  23. data/lib/jetstream_bridge/rails/railtie.rb +3 -4
  24. data/lib/jetstream_bridge/tasks/install.rake +17 -1
  25. data/lib/jetstream_bridge/topology/topology.rb +1 -6
  26. data/lib/jetstream_bridge/version.rb +1 -1
  27. data/lib/jetstream_bridge.rb +345 -202
  28. metadata +8 -8
  29. data/lib/jetstream_bridge/consumer/health_monitor.rb +0 -107
  30. data/lib/jetstream_bridge/core/connection_manager.rb +0 -513
  31. data/lib/jetstream_bridge/core/health_checker.rb +0 -184
  32. data/lib/jetstream_bridge/facade.rb +0 -212
  33. data/lib/jetstream_bridge/publisher/event_envelope_builder.rb +0 -110
data/docs/PRODUCTION.md CHANGED
@@ -78,9 +78,9 @@ JetstreamBridge.configure do |config|
78
78
  config.connect_retry_delay = 3 # Default: 2 seconds
79
79
 
80
80
  # Required configuration
81
+ config.stream_name = ENV.fetch("JETSTREAM_STREAM_NAME", "jetstream-bridge-stream")
81
82
  config.app_name = ENV.fetch("APP_NAME", "myapp")
82
83
  config.destination_app = ENV.fetch("DESTINATION_APP")
83
- config.stream_name = ENV.fetch("STREAM_NAME", "myapp-stream")
84
84
 
85
85
  # Enable reliability features
86
86
  config.use_outbox = true
@@ -100,31 +100,6 @@ JetstreamBridge.configure do |config|
100
100
  end
101
101
  ```
102
102
 
103
- ### Permissions and Inbox Prefix
104
-
105
- If your NATS account restricts `_INBOX.>` subscriptions, set an allowed prefix:
106
-
107
- ```ruby
108
- JetstreamBridge.configure do |config|
109
- config.inbox_prefix = "$RPC"
110
- end
111
- ```
112
-
113
- For pre-provisioned streams and consumers:
114
-
115
- ```ruby
116
- JetstreamBridge.configure do |config|
117
- config.stream_name = "my-stream" # required
118
- config.durable_name = "my-durable" # optional
119
- config.disable_js_api = true # skip JetStream management APIs
120
- end
121
- ```
122
-
123
- Minimum NATS permissions:
124
-
125
- - **Publish**: `$JS.API.>`, `$JS.ACK.>`, source/destination subjects, DLQ subject
126
- - **Subscribe**: `_INBOX.>` (or custom inbox_prefix), destination subject
127
-
128
103
  ---
129
104
 
130
105
  ## Consumer Tuning
@@ -167,7 +142,7 @@ Long-running consumers automatically:
167
142
 
168
143
  - Log health checks every 10 minutes (iterations, memory, uptime)
169
144
  - Warn when memory exceeds 1GB
170
- - Warn once when heap object counts grow large so you can profile/trigger GC in the host app
145
+ - Suggest garbage collection when heap grows large
171
146
 
172
147
  Monitor these logs to detect memory leaks early.
173
148
 
@@ -178,7 +153,7 @@ Monitor these logs to detect memory leaks early.
178
153
  ### Key Metrics to Track
179
154
 
180
155
  | Metric | Description | Alert Threshold |
181
- | -------- | ------------- | ----------------- |
156
+ | --- | --- | --- |
182
157
  | Consumer Lag | Pending messages in stream | > 1000 messages |
183
158
  | DLQ Size | Messages in dead letter queue | > 100 messages |
184
159
  | Connection Status | Health check failures | 2 consecutive failures |
@@ -194,7 +169,7 @@ Use the built-in health check for monitoring:
194
169
  # config/routes.rb
195
170
  Rails.application.routes.draw do
196
171
  get '/health/jetstream', to: proc { |env|
197
- health = JetstreamBridge.health
172
+ health = JetstreamBridge.health_check
198
173
  status = health[:healthy] ? 200 : 503
199
174
  [status, { 'Content-Type' => 'application/json' }, [health.to_json]]
200
175
  }
@@ -213,8 +188,8 @@ end
213
188
  },
214
189
  "stream": {
215
190
  "exists": true,
216
- "name": "production-jetstream-bridge-stream",
217
- "subjects": ["production.app.sync.worker"],
191
+ "name": "jetstream-bridge-stream",
192
+ "subjects": ["app.sync.worker"],
218
193
  "messages": 1523
219
194
  },
220
195
  "performance": {
@@ -222,15 +197,14 @@ end
222
197
  "health_check_duration_ms": 45.2
223
198
  },
224
199
  "config": {
225
- "env": "production",
200
+ "stream_name": "jetstream-bridge-stream",
226
201
  "app_name": "app",
227
202
  "destination_app": "worker",
228
203
  "use_outbox": true,
229
204
  "use_inbox": true,
230
- "use_dlq": true,
231
- "disable_js_api": true
205
+ "use_dlq": true
232
206
  },
233
- "version": "4.4.0"
207
+ "version": "4.0.3"
234
208
  }
235
209
  ```
236
210
 
@@ -267,6 +241,10 @@ jetstream_dlq = prometheus.gauge(
267
241
 
268
242
  ## Security Hardening
269
243
 
244
+ ### Permissions & Topology
245
+
246
+ Keep runtime credentials least-privileged: set `config.auto_provision = false` and provision the stream/consumer during deploy (`bundle exec rake jetstream_bridge:provision` or NATS CLI). The exact durable/subject layout and minimal publish/subscribe permissions live in [RESTRICTED_PERMISSIONS.md](RESTRICTED_PERMISSIONS.md). Health checks skip stream info when `auto_provision=false`; rely on your provisioning pipeline for validation.
247
+
270
248
  ### Rate Limiting
271
249
 
272
250
  The health check endpoint has built-in rate limiting (1 uncached request per 5 seconds). For HTTP endpoints, add additional protection:
@@ -0,0 +1,399 @@
1
+ # Working with Restricted NATS Permissions
2
+
3
+ This guide explains how to use JetStream Bridge when your NATS user lacks JetStream API permissions (`$JS.API.*` subjects).
4
+
5
+ ## Problem
6
+
7
+ When the NATS user has restricted permissions and cannot access JetStream API subjects, you'll see errors like:
8
+
9
+ ```markdown
10
+ ERROR -- : [JetstreamBridge::ConnectionManager] NATS error: 'Permissions Violation for Publish to "$JS.API.CONSUMER.INFO.{stream}.{consumer}"'
11
+ NATS::IO::Timeout: nats: timeout
12
+ ```
13
+
14
+ This happens because:
15
+
16
+ 1. JetStream Bridge tries to verify consumer configuration using `$JS.API.CONSUMER.INFO.*`
17
+ 2. The NATS user doesn't have permission to publish to these API subjects
18
+ 3. The connection is terminated, causing timeout errors
19
+
20
+ ## Solution Overview
21
+
22
+ When you cannot modify NATS server permissions, you need to:
23
+
24
+ 0. **Turn off runtime provisioning** so the app never calls `$JS.API.*`:
25
+ - Set `config.auto_provision = false`
26
+ - Provision stream + consumer once with admin credentials (CLI below or `bundle exec rake jetstream_bridge:provision`)
27
+ 1. **Pre-create the consumer** using a privileged NATS account
28
+ 2. **Ensure the consumer configuration matches** what your app expects
29
+
30
+ > Tip: When `auto_provision=false`, the app still connects/publishes/consumes but skips JetStream management APIs (account_info, stream_info, consumer_info). Health checks will report basic connectivity only.
31
+
32
+ ---
33
+
34
+ ## Runtime requirements (least privilege)
35
+
36
+ - Config: `config.auto_provision = false`, `config.stream_name` set explicitly.
37
+ - Topology: stream + durable consumer must be pre-provisioned (via `bundle exec rake jetstream_bridge:provision` or NATS CLI).
38
+ - NATS permissions for runtime creds:
39
+ - publish allow: `">"` (or narrowed to your business subjects) and `$JS.API.CONSUMER.MSG.NEXT.{stream_name}.{app_name}-workers`
40
+ - subscribe allow: `">"` (or narrowed) and `_INBOX.>` (responses for pull consumers)
41
+ - Health check will only report connectivity (stream info skipped).
42
+
43
+ ### Topology required
44
+
45
+ - Stream: `config.stream_name` (retention: workqueue, storage: file).
46
+ - Subjects:
47
+ - Publish: `{app_name}.sync.{destination_app}`
48
+ - Consume: `{destination_app}.sync.{app_name}`
49
+ - DLQ (if enabled): `{app_name}.sync.dlq`
50
+ - Durable consumer: `{app_name}-workers` filtering on `{destination_app}.sync.{app_name}`.
51
+
52
+ ---
53
+
54
+ ## Option A: Provision with the built-in task (creates stream + consumer)
55
+
56
+ Run this from CI/deploy with admin NATS credentials:
57
+
58
+ ```bash
59
+ # Uses your configured stream/app/destination to create the stream + durable consumer
60
+ NATS_URLS="nats://admin:pass@10.199.12.34:4222" \
61
+ bundle exec rake jetstream_bridge:provision
62
+ ```
63
+
64
+ This is the easiest way to keep `auto_provision=false` in runtime while still reusing the bridge’s topology logic (subjects, DLQ, overlap guard).
65
+
66
+ ## Option B: Pre-create the Consumer using NATS CLI
67
+
68
+ ### Install NATS CLI
69
+
70
+ ```bash
71
+ # Download from https://github.com/nats-io/natscli/releases
72
+ curl -sf https://binaries.nats.dev/nats-io/natscli/nats@latest | sh
73
+
74
+ # Or using Homebrew (macOS)
75
+ brew install nats-io/nats-tools/nats
76
+ ```
77
+
78
+ ### Create the Consumer
79
+
80
+ You need to create a durable pull consumer with the exact configuration your app expects.
81
+
82
+ **Required values from your JetStream Bridge config:**
83
+
84
+ - **Stream name**: `JETSTREAM_STREAM_NAME` (e.g., `jetstream-bridge-stream`)
85
+ - **Consumer name**: `{app_name}-workers` (e.g., `pwas-workers`)
86
+ - **Filter subject**: `{app_name}.sync.{destination_app}` (e.g., `pwas-workers.sync.heavyworth`)
87
+
88
+ **Create consumer command:**
89
+
90
+ ```bash
91
+ # Connect using a privileged NATS account
92
+ nats context save admin \
93
+ --server=nats://admin-user:admin-pass@10.199.12.34:4222 \
94
+ --description="Admin account for consumer creation"
95
+
96
+ # Select the context
97
+ nats context select admin
98
+
99
+ # Create the consumer
100
+ nats consumer add production-jetstream-bridge-stream production-pwas-workers \
101
+ --filter "production.pwas-workers.sync.heavyworth" \
102
+ --ack explicit \
103
+ --pull \
104
+ --deliver all \
105
+ --max-deliver 5 \
106
+ --ack-wait 30s \
107
+ --replay instant \
108
+ --max-pending 25000
109
+ ```
110
+
111
+ **With backoff (recommended for production):**
112
+
113
+ ```bash
114
+ nats consumer add production-jetstream-bridge-stream production-pwas-workers \
115
+ --filter "production.pwas-workers.sync.heavyworth" \
116
+ --ack explicit \
117
+ --pull \
118
+ --deliver all \
119
+ --max-deliver 5 \
120
+ --ack-wait 30s \
121
+ --backoff 1s,5s,15s,30s,60s \
122
+ --replay instant \
123
+ --max-pending 25000
124
+ ```
125
+
126
+ ### Verify Consumer Creation
127
+
128
+ ```bash
129
+ nats consumer info production-jetstream-bridge-stream production-pwas-workers
130
+ ```
131
+
132
+ Expected output:
133
+
134
+ ```bash
135
+ Information for Consumer production-jetstream-bridge-stream > production-pwas-workers
136
+
137
+ Configuration:
138
+
139
+ Durable Name: production-pwas-workers
140
+ Filter Subject: production.pwas-workers.sync.heavyworth
141
+ Ack Policy: explicit
142
+ Ack Wait: 30s
143
+ Replay Policy: instant
144
+ Maximum Deliveries: 5
145
+ Backoff: [1s 5s 15s 30s 60s]
146
+
147
+ State:
148
+
149
+ Last Delivered Message: Consumer sequence: 0 Stream sequence: 0
150
+ Acknowledgment Floor: Consumer sequence: 0 Stream sequence: 0
151
+ Pending Messages: 0
152
+ Redelivered Messages: 0
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Step 2: Configure JetStream Bridge
158
+
159
+ Configure JetStream Bridge to avoid JetStream management APIs at runtime (pre-provisioning handles them instead):
160
+
161
+ ```ruby
162
+ # config/initializers/jetstream_bridge.rb
163
+ JetstreamBridge.configure do |config|
164
+ config.nats_urls = ENV.fetch("NATS_URLS")
165
+ config.stream_name = "jetstream-bridge-stream"
166
+ config.app_name = "pwas-workers"
167
+ config.destination_app = "heavyworth"
168
+ config.auto_provision = false
169
+
170
+ config.use_outbox = true
171
+ config.use_inbox = true
172
+ config.use_dlq = true
173
+
174
+ # These settings MUST match the pre-created consumer
175
+ config.max_deliver = 5
176
+ config.ack_wait = "30s"
177
+ config.backoff = %w[1s 5s 15s 30s 60s]
178
+ end
179
+ ```
180
+
181
+ **Critical:** The `max_deliver`, `ack_wait`, and `backoff` values in your config **must exactly match** the pre-created consumer configuration. If they don't match, messages may be redelivered incorrectly.
182
+
183
+ ---
184
+
185
+ ## Step 3: Deploy and Verify
186
+
187
+ ### Deploy the Updated Gem
188
+
189
+ If you're working on the jetstream_bridge gem itself:
190
+
191
+ ```bash
192
+ # Build the gem
193
+ gem build jetstream_bridge.gemspec
194
+
195
+ # Install locally for testing
196
+ gem install ./jetstream_bridge-4.5.0.gem
197
+
198
+ # Or update in your application's Gemfile.lock
199
+ bundle update jetstream_bridge
200
+ ```
201
+
202
+ If this is a local modification, you can point your Gemfile to the local path:
203
+
204
+ ```ruby
205
+ # Gemfile (temporary for testing)
206
+ gem 'jetstream_bridge', path: '/path/to/local/jetstream_bridge'
207
+ ```
208
+
209
+ ### Restart the Service
210
+
211
+ ```bash
212
+ sudo systemctl restart pwas_production_sync
213
+ ```
214
+
215
+ ### Monitor the Logs
216
+
217
+ ```bash
218
+ sudo journalctl -u pwas_production_sync -f
219
+ ```
220
+
221
+ **Expected success output:**
222
+
223
+ ```bash
224
+ INFO -- : [JetstreamBridge::ConnectionManager] Connected to NATS (1 server): nats://pwas:***@10.199.12.34:4222
225
+ INFO -- : [DataSync] Consumer starting (durable=production-pwas-workers, batch=25, dest="heavyworth")
226
+ INFO -- : [DataSync] run! started successfully
227
+ ```
228
+
229
+ Health checks will report connectivity only when `auto_provision=false` (stream info is skipped to avoid `$JS.API.STREAM.INFO`).
230
+
231
+ **If you still see errors:**
232
+
233
+ 1. **Timeout during subscribe** - The pre-created consumer name/filter might not match. Verify:
234
+
235
+ ```bash
236
+ nats consumer ls production-jetstream-bridge-stream
237
+ ```
238
+
239
+ 2. **Permission violations on fetch** - The NATS user also needs permission to fetch messages. Minimum required:
240
+
241
+ ```conf
242
+ subscribe: {
243
+ allow: ["production.>", "_INBOX.>"]
244
+ }
245
+ ```
246
+
247
+ ## Maintenance
248
+
249
+ ### Updating Consumer Configuration
250
+
251
+ If you need to change consumer settings (e.g., increase `max_deliver`):
252
+
253
+ 1. **Stop your application** to prevent message processing
254
+
255
+ ```bash
256
+ sudo systemctl stop pwas_production_sync
257
+ ```
258
+
259
+ 2. **Delete the old consumer** (using privileged account)
260
+
261
+ ```bash
262
+ nats consumer rm production-jetstream-bridge-stream production-pwas-workers -f
263
+ ```
264
+
265
+ 3. **Create the new consumer** with updated settings
266
+
267
+ ```bash
268
+ nats consumer add production-jetstream-bridge-stream production-pwas-workers \
269
+ --filter "production.pwas-workers.sync.heavyworth" \
270
+ --ack explicit \
271
+ --pull \
272
+ --deliver all \
273
+ --max-deliver 10 \
274
+ --ack-wait 60s \
275
+ --backoff 2s,10s,30s,60s,120s \
276
+ --replay instant \
277
+ --max-pending 25000
278
+ ```
279
+
280
+ 4. **Update your application config** to match
281
+
282
+ ```ruby
283
+ config.max_deliver = 10
284
+ config.ack_wait = "60s"
285
+ config.backoff = %w[2s 10s 30s 60s 120s]
286
+ ```
287
+
288
+ 5. **Restart your application**
289
+
290
+ ```bash
291
+ sudo systemctl start pwas_production_sync
292
+ ```
293
+
294
+ ### Monitoring
295
+
296
+ Add monitoring to detect configuration drift:
297
+
298
+ ```ruby
299
+ # Check consumer exists and is healthy
300
+ health = JetstreamBridge.health_check
301
+ unless health[:healthy]
302
+ alert("JetStream consumer unhealthy: #{health}")
303
+ end
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Troubleshooting
309
+
310
+ ### Consumer doesn't receive messages
311
+
312
+ **Check stream has messages:**
313
+
314
+ ```bash
315
+ nats stream info production-jetstream-bridge-stream
316
+ ```
317
+
318
+ **Verify filter subject matches your topology:**
319
+
320
+ ```bash
321
+ # App publishes to: {app_name}.sync.{destination_app}
322
+ # Consumer filters on: {destination_app}.sync.{app_name}
323
+ ```
324
+
325
+ ### Service keeps restarting
326
+
327
+ Check if the issue is:
328
+
329
+ 1. **Consumer doesn't exist** - Pre-create it with NATS CLI
330
+ 2. **Filter subject mismatch** - Verify with `nats consumer info`
331
+ 3. **Permissions still insufficient** - User needs subscribe permissions on the filtered subject
332
+ 4. **Wrong stream name** - Ensure stream name matches your configured `config.stream_name`
333
+
334
+ ---
335
+
336
+ ## Security Considerations
337
+
338
+ 1. **Consumer pre-creation requires privileged access** - Keep admin credentials secure
339
+ 2. **Configuration drift risk** - If app config doesn't match consumer config, message delivery may fail silently
340
+ 3. **No automatic recovery** - If consumer is deleted, it won't be recreated automatically
341
+ 4. **Consider automation** - Use infrastructure-as-code (Terraform, Ansible) to manage consumer creation
342
+
343
+ ---
344
+
345
+ ## Example: Production Setup for pwas-api
346
+
347
+ Based on your logs, here's the exact setup:
348
+
349
+ ```bash
350
+ # 1. Pre-create the consumer (as admin)
351
+ nats consumer add pwas-heavyworth-sync production-pwas-workers \
352
+ --filter "production.pwas-workers.sync.heavyworth" \
353
+ --ack explicit \
354
+ --pull \
355
+ --deliver all \
356
+ --max-deliver 5 \
357
+ --ack-wait 30s \
358
+ --backoff 1s,5s,15s,30s,60s \
359
+ --replay instant
360
+ ```
361
+
362
+ ```ruby
363
+ # 2. Update config/initializers/jetstream_bridge.rb
364
+ JetstreamBridge.configure do |config|
365
+ config.nats_urls = "nats://pwas:***@10.199.12.34:4222"
366
+ config.stream_name = "jetstream-bridge-stream"
367
+ config.app_name = "pwas-workers"
368
+ config.destination_app = "heavyworth"
369
+ config.max_deliver = 5
370
+ config.ack_wait = "30s"
371
+ config.backoff = %w[1s 5s 15s 30s 60s]
372
+ end
373
+ ```
374
+
375
+ **Note:** Verify your exact stream name and consumer durable (`app_name-workers`) match what was provisioned.
376
+
377
+ ---
378
+
379
+ ## Alternative: Request Minimal Permissions
380
+
381
+ If you have any influence over NATS permissions, request only these minimal subjects:
382
+
383
+ ```conf
384
+ # Minimal permissions needed for JetStream Bridge consumer
385
+ publish: {
386
+ allow: [
387
+ ">", # Your app subjects (narrow if desired)
388
+ "$JS.API.CONSUMER.MSG.NEXT.{stream_name}.{app_name}-workers", # Fetch messages (required)
389
+ ]
390
+ }
391
+ subscribe: {
392
+ allow: [
393
+ ">", # Your app subjects (narrow if desired)
394
+ "_INBOX.>", # Request-reply responses (required)
395
+ ]
396
+ }
397
+ ```
398
+
399
+ These are read-only operations and don't allow creating/modifying streams or consumers.
data/docs/TESTING.md CHANGED
@@ -28,28 +28,31 @@ RSpec.describe MyService do
28
28
  include JetstreamBridge::TestHelpers::Matchers
29
29
 
30
30
  before do
31
- # Reset state
31
+ # Reset singleton to ensure clean state
32
+ JetstreamBridge::Connection.instance_variable_set(:@singleton__instance__, nil)
32
33
  JetstreamBridge.reset!
33
34
 
34
35
  # Enable test mode - automatically sets up mock NATS
35
36
  JetstreamBridge::TestHelpers.enable_test_mode!
36
37
 
37
38
  JetstreamBridge.configure do |config|
39
+ config.stream_name = 'jetstream-bridge-stream'
38
40
  config.app_name = 'my_app'
39
41
  config.destination_app = 'worker'
40
- config.stream_name = 'test-stream'
41
42
  end
42
43
 
43
- # Setup mock stream
44
+ # Setup mock stream and stub topology
44
45
  mock_jts = JetstreamBridge::TestHelpers.mock_connection.jetstream
45
46
  mock_jts.add_stream(
46
- name: 'test-stream',
47
- subjects: ['my_app.sync.worker', 'worker.sync.my_app']
47
+ name: 'test-jetstream-bridge-stream',
48
+ subjects: ['test.>']
48
49
  )
50
+ allow(JetstreamBridge::Topology).to receive(:ensure!)
49
51
  end
50
52
 
51
53
  after do
52
54
  JetstreamBridge::TestHelpers.reset_test_mode!
55
+ JetstreamBridge::Connection.instance_variable_set(:@singleton__instance__, nil)
53
56
  end
54
57
 
55
58
  it 'publishes events through the full stack' do
@@ -263,20 +266,27 @@ end.to raise_error(NATS::JetStream::Error, 'consumer not found')
263
266
  ```ruby
264
267
  before do
265
268
  JetstreamBridge.reset!
266
- JetstreamBridge::TestHelpers.enable_test_mode!
269
+
270
+ mock_conn = JetstreamBridge::TestHelpers::MockNats.create_mock_connection
271
+ mock_jts = mock_conn.jetstream
272
+
273
+ allow(NATS::IO::Client).to receive(:new).and_return(mock_conn)
274
+ allow(JetstreamBridge::Connection).to receive(:connect!).and_call_original
275
+
276
+ # Setup stream
277
+ mock_jts.add_stream(
278
+ name: 'test-jetstream-bridge-stream',
279
+ subjects: ['test.>']
280
+ )
281
+
282
+ # Allow topology check to succeed
283
+ allow(JetstreamBridge::Topology).to receive(:ensure!)
267
284
 
268
285
  JetstreamBridge.configure do |config|
286
+ config.stream_name = 'jetstream-bridge-stream'
269
287
  config.app_name = 'api'
270
288
  config.destination_app = 'worker'
271
- config.stream_name = 'test-stream'
272
289
  end
273
-
274
- # Setup mock stream
275
- mock_jts = JetstreamBridge::TestHelpers.mock_connection.jetstream
276
- mock_jts.add_stream(
277
- name: 'test-stream',
278
- subjects: ['api.sync.worker', 'worker.sync.api']
279
- )
280
290
  end
281
291
 
282
292
  it 'publishes through JetstreamBridge' do
@@ -286,9 +296,9 @@ it 'publishes through JetstreamBridge' do
286
296
  payload: { id: 1, name: 'Test' }
287
297
  )
288
298
 
289
- expect(result.success?).to be true
299
+ expect(result).to be_publish_success
290
300
  expect(result.event_id).to be_present
291
- expect(result.subject).to eq('api.sync.worker')
301
+ expect(result.subject).to eq('api.sync.worker')
292
302
 
293
303
  # Verify in storage
294
304
  storage = JetstreamBridge::TestHelpers.mock_storage
@@ -296,11 +306,12 @@ it 'publishes through JetstreamBridge' do
296
306
  end
297
307
  ```
298
308
 
299
- ### Full Consumer Flow
309
+ ### Full Consuming Flow
300
310
 
301
311
  ```ruby
302
- it 'consumes messages through JetstreamBridge' do
303
- mock_jts = JetstreamBridge::TestHelpers.mock_connection.jetstream
312
+ it 'consumes through JetstreamBridge' do
313
+ mock_conn = JetstreamBridge::TestHelpers.mock_connection
314
+ mock_jts = mock_conn.jetstream
304
315
 
305
316
  # Publish message to destination subject
306
317
  mock_jts.publish(
@@ -328,8 +339,8 @@ it 'consumes messages through JetstreamBridge' do
328
339
  # Mock subscription
329
340
  subscription = mock_jts.pull_subscribe(
330
341
  'worker.sync.api',
331
- 'api-workers',
332
- stream: 'test-stream'
342
+ 'test-consumer',
343
+ stream: 'test-jetstream-bridge-stream'
333
344
  )
334
345
 
335
346
  allow_any_instance_of(JetstreamBridge::SubscriptionManager)
@@ -380,7 +391,7 @@ storage.reset!
380
391
  3. **Test both success and failure paths**: Use the mock to simulate errors
381
392
  4. **Verify message content**: Check that envelopes are correctly formatted
382
393
  5. **Test idempotency**: Verify duplicate detection and redelivery behavior
383
- 6. **Set stream_name**: Always configure `stream_name` in your tests
394
+ 6. **Mock topology setup**: Remember to stub `JetstreamBridge::Topology.ensure!`
384
395
 
385
396
  ## Examples
386
397
 
@@ -41,12 +41,12 @@ module JetstreamBridge
41
41
  "connected_at": "2025-11-22T20:00:00Z",
42
42
  "stream": {
43
43
  "exists": true,
44
- "name": "development-jetstream-bridge-stream",
45
- "subjects": ["dev.app1.sync.app2"],
44
+ "name": "jetstream-bridge-stream",
45
+ "subjects": ["app1.sync.app2"],
46
46
  "messages": 42
47
47
  },
48
48
  "config": {
49
- "env": "development",
49
+ "stream_name": "jetstream-bridge-stream",
50
50
  "app_name": "my_app",
51
51
  "destination_app": "other_app"
52
52
  },
@@ -13,6 +13,9 @@ JetstreamBridge.configure do |config|
13
13
  # NATS server URLs (comma-separated for cluster)
14
14
  config.nats_urls = ENV.fetch('NATS_URLS', 'nats://localhost:4222')
15
15
 
16
+ # Stream name (required) - managed separately from runtime credentials
17
+ config.stream_name = ENV.fetch('JETSTREAM_STREAM_NAME', 'jetstream-bridge-stream')
18
+
16
19
  # Application name (used in subject routing)
17
20
  config.app_name = ENV.fetch('APP_NAME', Rails.application.class.module_parent_name.underscore)
18
21