jetstream_bridge 4.1.0 → 4.2.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/CHANGELOG.md +50 -0
- data/README.md +22 -1427
- data/docs/GETTING_STARTED.md +92 -0
- data/docs/PRODUCTION.md +503 -0
- data/docs/TESTING.md +414 -0
- data/lib/jetstream_bridge/consumer/consumer.rb +16 -4
- data/lib/jetstream_bridge/consumer/inbox/inbox_processor.rb +17 -3
- data/lib/jetstream_bridge/consumer/inbox/inbox_repository.rb +19 -7
- data/lib/jetstream_bridge/consumer/message_processor.rb +88 -52
- data/lib/jetstream_bridge/consumer/subscription_manager.rb +24 -15
- data/lib/jetstream_bridge/core/bridge_helpers.rb +85 -0
- data/lib/jetstream_bridge/core/connection.rb +18 -1
- data/lib/jetstream_bridge/core.rb +8 -0
- data/lib/jetstream_bridge/models/inbox_event.rb +2 -2
- data/lib/jetstream_bridge/models/outbox_event.rb +2 -2
- data/lib/jetstream_bridge/publisher/publisher.rb +8 -6
- data/lib/jetstream_bridge/rails/integration.rb +153 -0
- data/lib/jetstream_bridge/rails/railtie.rb +53 -0
- data/lib/jetstream_bridge/rails.rb +5 -0
- data/lib/jetstream_bridge/tasks/install.rake +1 -1
- data/lib/jetstream_bridge/test_helpers/fixtures.rb +41 -0
- data/lib/jetstream_bridge/test_helpers/integration_helpers.rb +77 -0
- data/lib/jetstream_bridge/test_helpers/matchers.rb +98 -0
- data/lib/jetstream_bridge/test_helpers.rb +4 -259
- data/lib/jetstream_bridge/topology/stream.rb +1 -1
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +51 -93
- metadata +21 -8
- data/lib/jetstream_bridge/railtie.rb +0 -91
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'integration'
|
|
4
|
+
|
|
5
|
+
module JetstreamBridge
|
|
6
|
+
# Rails integration for JetStream Bridge.
|
|
7
|
+
#
|
|
8
|
+
# This Railtie integrates JetStream Bridge with the Rails application lifecycle:
|
|
9
|
+
# - Configuration: Logger is configured early in the Rails boot process
|
|
10
|
+
# - Startup: Connection is established after user initializers load (explicit startup!)
|
|
11
|
+
# - Shutdown: Connection is closed when Rails shuts down (at_exit hook)
|
|
12
|
+
# - Restart: Puma/Unicorn workers get fresh connections on fork
|
|
13
|
+
#
|
|
14
|
+
class Railtie < ::Rails::Railtie
|
|
15
|
+
# Set up logger to use Rails.logger by default
|
|
16
|
+
# Note: This only configures the logger, does NOT establish connection
|
|
17
|
+
initializer 'jetstream_bridge.logger', before: :initialize_logger do
|
|
18
|
+
JetstreamBridge::Rails::Integration.configure_logger!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Load ActiveRecord model tweaks after ActiveRecord is loaded
|
|
22
|
+
initializer 'jetstream_bridge.active_record', after: 'active_record.initialize_database' do
|
|
23
|
+
JetstreamBridge::Rails::Integration.attach_active_record_hooks!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Establish connection after Rails initialization is complete
|
|
27
|
+
# This runs after all user initializers have loaded
|
|
28
|
+
config.after_initialize do
|
|
29
|
+
JetstreamBridge::Rails::Integration.boot_bridge!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Add console helper methods
|
|
33
|
+
console do
|
|
34
|
+
::Rails.logger.info "[JetStream Bridge] Loaded v#{JetstreamBridge::VERSION}"
|
|
35
|
+
::Rails.logger.info '[JetStream Bridge] Console helpers available:'
|
|
36
|
+
::Rails.logger.info ' JetstreamBridge.health_check - Check connection status'
|
|
37
|
+
::Rails.logger.info ' JetstreamBridge.stream_info - View stream details'
|
|
38
|
+
::Rails.logger.info ' JetstreamBridge.connected? - Check if connected'
|
|
39
|
+
::Rails.logger.info ' JetstreamBridge.shutdown! - Gracefully disconnect'
|
|
40
|
+
::Rails.logger.info ' JetstreamBridge.reconnect! - Reconnect (useful after configuration changes)'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Load rake tasks
|
|
44
|
+
rake_tasks do
|
|
45
|
+
load File.expand_path('../tasks/install.rake', __dir__)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Add generators
|
|
49
|
+
generators do
|
|
50
|
+
require 'generators/jetstream_bridge/health_check/health_check_generator'
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -85,7 +85,7 @@ namespace :jetstream_bridge do
|
|
|
85
85
|
puts '[jetstream_bridge] Testing NATS connection...'
|
|
86
86
|
|
|
87
87
|
begin
|
|
88
|
-
jts = JetstreamBridge.
|
|
88
|
+
jts = JetstreamBridge.connect_and_ensure_stream!
|
|
89
89
|
puts '✓ Successfully connected to NATS'
|
|
90
90
|
puts '✓ JetStream is available'
|
|
91
91
|
puts '✓ Stream topology ensured'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JetstreamBridge
|
|
4
|
+
module TestHelpers
|
|
5
|
+
# Common fixtures for quickly building events in specs.
|
|
6
|
+
module Fixtures
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Build a user.created event
|
|
10
|
+
def user_created_event(attrs = {})
|
|
11
|
+
JetstreamBridge::TestHelpers.build_jetstream_event(
|
|
12
|
+
event_type: 'user.created',
|
|
13
|
+
payload: {
|
|
14
|
+
id: attrs[:id] || 1,
|
|
15
|
+
email: attrs[:email] || 'test@example.com',
|
|
16
|
+
name: attrs[:name] || 'Test User'
|
|
17
|
+
}.merge(attrs[:payload] || {})
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Build multiple sample events
|
|
22
|
+
def sample_events(count = 3, type: 'test.event')
|
|
23
|
+
Array.new(count) do |i|
|
|
24
|
+
JetstreamBridge::TestHelpers.build_jetstream_event(
|
|
25
|
+
event_type: type,
|
|
26
|
+
payload: { id: i + 1, sequence: i }
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Build a generic event with custom attributes
|
|
32
|
+
def event(event_type:, payload: {}, **attrs)
|
|
33
|
+
JetstreamBridge::TestHelpers.build_jetstream_event(
|
|
34
|
+
event_type: event_type,
|
|
35
|
+
payload: payload,
|
|
36
|
+
**attrs
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JetstreamBridge
|
|
4
|
+
module TestHelpers
|
|
5
|
+
# Integration helpers to exercise the mock NATS storage end-to-end.
|
|
6
|
+
module IntegrationHelpers
|
|
7
|
+
# Publish an event and wait for it to appear in mock storage
|
|
8
|
+
#
|
|
9
|
+
# @param event_attrs [Hash] Event attributes to publish
|
|
10
|
+
# @param timeout [Integer] Maximum seconds to wait
|
|
11
|
+
# @return [Models::PublishResult] Publish result
|
|
12
|
+
# @raise [Timeout::Error] If event doesn't appear within timeout
|
|
13
|
+
def publish_and_wait(timeout: 1, **event_attrs)
|
|
14
|
+
result = JetstreamBridge.publish(**event_attrs)
|
|
15
|
+
|
|
16
|
+
deadline = Time.now + timeout
|
|
17
|
+
until Time.now > deadline
|
|
18
|
+
storage = JetstreamBridge::TestHelpers.mock_storage
|
|
19
|
+
break if storage.messages.any? { |m| m[:header]['nats-msg-id'] == result.event_id }
|
|
20
|
+
|
|
21
|
+
sleep 0.01
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Consume events from mock storage
|
|
28
|
+
#
|
|
29
|
+
# @param batch_size [Integer] Number of events to consume
|
|
30
|
+
# @yield [event] Block to handle each event
|
|
31
|
+
# @return [Array<Hash>] Consumed events
|
|
32
|
+
def consume_events(batch_size: 10, &handler)
|
|
33
|
+
storage = JetstreamBridge::TestHelpers.mock_storage
|
|
34
|
+
messages = storage.messages.first(batch_size)
|
|
35
|
+
|
|
36
|
+
messages.each do |msg|
|
|
37
|
+
event = JetstreamBridge::Models::Event.from_nats_message(
|
|
38
|
+
OpenStruct.new(
|
|
39
|
+
subject: msg[:subject],
|
|
40
|
+
data: msg[:data],
|
|
41
|
+
header: msg[:header],
|
|
42
|
+
metadata: OpenStruct.new(
|
|
43
|
+
sequence: OpenStruct.new(stream: msg[:sequence]),
|
|
44
|
+
num_delivered: msg[:delivery_count],
|
|
45
|
+
stream: 'test-stream',
|
|
46
|
+
consumer: 'test-consumer'
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
handler&.call(event)
|
|
52
|
+
JetstreamBridge::TestHelpers.record_consumed_event(event.to_h)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
JetstreamBridge::TestHelpers.consumed_events
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Wait for a specific number of messages in mock storage
|
|
59
|
+
#
|
|
60
|
+
# @param count [Integer] Expected message count
|
|
61
|
+
# @param timeout [Integer] Maximum seconds to wait
|
|
62
|
+
# @return [Boolean] true if count reached, false if timeout
|
|
63
|
+
def wait_for_messages(count, timeout: 2)
|
|
64
|
+
deadline = Time.now + timeout
|
|
65
|
+
storage = JetstreamBridge::TestHelpers.mock_storage
|
|
66
|
+
|
|
67
|
+
until Time.now > deadline
|
|
68
|
+
return true if storage.messages.size >= count
|
|
69
|
+
|
|
70
|
+
sleep 0.01
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JetstreamBridge
|
|
4
|
+
module TestHelpers
|
|
5
|
+
# RSpec matchers for asserting publish outcomes and captured events.
|
|
6
|
+
module Matchers
|
|
7
|
+
# Matcher for checking if an event was published
|
|
8
|
+
#
|
|
9
|
+
# @param event_type [String] Event type to match
|
|
10
|
+
# @param payload [Hash] Optional payload attributes to match
|
|
11
|
+
# @return [HavePublished] Matcher instance
|
|
12
|
+
def have_published(event_type:, payload: {})
|
|
13
|
+
HavePublished.new(event_type, payload)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Matcher implementation for have_published
|
|
17
|
+
class HavePublished
|
|
18
|
+
def initialize(event_type, payload_attributes)
|
|
19
|
+
@event_type = event_type
|
|
20
|
+
@payload_attributes = payload_attributes
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def matches?(_actual)
|
|
24
|
+
TestHelpers.published_events.any? do |event|
|
|
25
|
+
matches_event_type?(event) && matches_payload?(event)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def failure_message
|
|
30
|
+
"expected to have published event_type: #{@event_type.inspect} " \
|
|
31
|
+
"with payload: #{@payload_attributes.inspect}\n" \
|
|
32
|
+
"but found events: #{TestHelpers.published_events.map { |e| e['event_type'] }.inspect}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def failure_message_when_negated
|
|
36
|
+
"expected not to have published event_type: #{@event_type.inspect} " \
|
|
37
|
+
"with payload: #{@payload_attributes.inspect}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def matches_event_type?(event)
|
|
43
|
+
event['event_type'] == @event_type || event[:event_type] == @event_type
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def matches_payload?(event)
|
|
47
|
+
payload = event['payload'] || event[:payload] || {}
|
|
48
|
+
@payload_attributes.all? do |key, value|
|
|
49
|
+
payload_value = payload[key.to_s] || payload[key.to_sym]
|
|
50
|
+
if value.is_a?(RSpec::Matchers::BuiltIn::BaseMatcher)
|
|
51
|
+
value.matches?(payload)
|
|
52
|
+
else
|
|
53
|
+
payload_value == value
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Matcher for checking publish result success
|
|
60
|
+
def be_publish_success
|
|
61
|
+
BePublishSuccess.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class BePublishSuccess
|
|
65
|
+
def matches?(actual)
|
|
66
|
+
actual.respond_to?(:success?) && actual.success?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def failure_message
|
|
70
|
+
'expected PublishResult to be successful but it failed'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def failure_message_when_negated
|
|
74
|
+
'expected PublishResult to not be successful but it was'
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Matcher for checking publish result failure
|
|
79
|
+
def be_publish_failure
|
|
80
|
+
BePublishFailure.new
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class BePublishFailure
|
|
84
|
+
def matches?(actual)
|
|
85
|
+
actual.respond_to?(:failure?) && actual.failure?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def failure_message
|
|
89
|
+
'expected PublishResult to be a failure but it succeeded'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def failure_message_when_negated
|
|
93
|
+
'expected PublishResult to not be a failure but it was'
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require 'securerandom'
|
|
4
4
|
require_relative 'test_helpers/mock_nats'
|
|
5
|
+
require_relative 'test_helpers/matchers'
|
|
6
|
+
require_relative 'test_helpers/fixtures'
|
|
7
|
+
require_relative 'test_helpers/integration_helpers'
|
|
5
8
|
|
|
6
9
|
module JetstreamBridge
|
|
7
10
|
# Test helpers for easier testing of JetStream Bridge integrations
|
|
@@ -210,6 +213,7 @@ module JetstreamBridge
|
|
|
210
213
|
}
|
|
211
214
|
)
|
|
212
215
|
end
|
|
216
|
+
module_function :build_jetstream_event
|
|
213
217
|
|
|
214
218
|
# Simulate triggering an event to a consumer
|
|
215
219
|
#
|
|
@@ -228,265 +232,6 @@ module JetstreamBridge
|
|
|
228
232
|
TestHelpers.record_consumed_event(event.to_h) if TestHelpers.test_mode?
|
|
229
233
|
handler.call(event)
|
|
230
234
|
end
|
|
231
|
-
|
|
232
|
-
# RSpec matchers module
|
|
233
|
-
#
|
|
234
|
-
# @example Include in RSpec
|
|
235
|
-
# RSpec.configure do |config|
|
|
236
|
-
# config.include JetstreamBridge::TestHelpers
|
|
237
|
-
# config.include JetstreamBridge::TestHelpers::Matchers
|
|
238
|
-
# end
|
|
239
|
-
#
|
|
240
|
-
module Matchers
|
|
241
|
-
# Matcher for checking if an event was published
|
|
242
|
-
#
|
|
243
|
-
# @param event_type [String] Event type to match
|
|
244
|
-
# @param payload [Hash] Optional payload attributes to match
|
|
245
|
-
# @return [HavePublished] Matcher instance
|
|
246
|
-
#
|
|
247
|
-
# @example
|
|
248
|
-
# expect(JetstreamBridge).to have_published(
|
|
249
|
-
# event_type: "user.created",
|
|
250
|
-
# payload: { id: 1 }
|
|
251
|
-
# )
|
|
252
|
-
#
|
|
253
|
-
def have_published(event_type:, payload: {})
|
|
254
|
-
HavePublished.new(event_type, payload)
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
# Matcher implementation for have_published
|
|
258
|
-
class HavePublished
|
|
259
|
-
def initialize(event_type, payload_attributes)
|
|
260
|
-
@event_type = event_type
|
|
261
|
-
@payload_attributes = payload_attributes
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def matches?(_actual)
|
|
265
|
-
TestHelpers.published_events.any? do |event|
|
|
266
|
-
matches_event_type?(event) && matches_payload?(event)
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def failure_message
|
|
271
|
-
"expected to have published event_type: #{@event_type.inspect} " \
|
|
272
|
-
"with payload: #{@payload_attributes.inspect}\n" \
|
|
273
|
-
"but found events: #{TestHelpers.published_events.map { |e| e['event_type'] }.inspect}"
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def failure_message_when_negated
|
|
277
|
-
"expected not to have published event_type: #{@event_type.inspect} " \
|
|
278
|
-
"with payload: #{@payload_attributes.inspect}"
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
private
|
|
282
|
-
|
|
283
|
-
def matches_event_type?(event)
|
|
284
|
-
event['event_type'] == @event_type || event[:event_type] == @event_type
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
def matches_payload?(event)
|
|
288
|
-
payload = event['payload'] || event[:payload] || {}
|
|
289
|
-
@payload_attributes.all? do |key, value|
|
|
290
|
-
payload_value = payload[key.to_s] || payload[key.to_sym]
|
|
291
|
-
if value.is_a?(RSpec::Matchers::BuiltIn::BaseMatcher)
|
|
292
|
-
value.matches?(payload)
|
|
293
|
-
else
|
|
294
|
-
payload_value == value
|
|
295
|
-
end
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
# Matcher for checking publish result
|
|
301
|
-
#
|
|
302
|
-
# @example
|
|
303
|
-
# result = JetstreamBridge.publish(...)
|
|
304
|
-
# expect(result).to be_publish_success
|
|
305
|
-
#
|
|
306
|
-
def be_publish_success
|
|
307
|
-
BePublishSuccess.new
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
# Matcher implementation for be_publish_success
|
|
311
|
-
class BePublishSuccess
|
|
312
|
-
def matches?(actual)
|
|
313
|
-
actual.respond_to?(:success?) && actual.success?
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
def failure_message
|
|
317
|
-
'expected PublishResult to be successful but it failed'
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
def failure_message_when_negated
|
|
321
|
-
'expected PublishResult to not be successful but it was'
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
# Matcher for checking publish failure
|
|
326
|
-
#
|
|
327
|
-
# @example
|
|
328
|
-
# result = JetstreamBridge.publish(...)
|
|
329
|
-
# expect(result).to be_publish_failure
|
|
330
|
-
#
|
|
331
|
-
def be_publish_failure
|
|
332
|
-
BePublishFailure.new
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
# Matcher implementation for be_publish_failure
|
|
336
|
-
class BePublishFailure
|
|
337
|
-
def matches?(actual)
|
|
338
|
-
actual.respond_to?(:failure?) && actual.failure?
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
def failure_message
|
|
342
|
-
'expected PublishResult to be a failure but it succeeded'
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
def failure_message_when_negated
|
|
346
|
-
'expected PublishResult to not be a failure but it was'
|
|
347
|
-
end
|
|
348
|
-
end
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
# Test fixtures for common event scenarios
|
|
352
|
-
#
|
|
353
|
-
# @example Create a user.created event
|
|
354
|
-
# event = JetstreamBridge::TestHelpers::Fixtures.user_created_event(id: 123)
|
|
355
|
-
#
|
|
356
|
-
module Fixtures
|
|
357
|
-
# Build a user.created event
|
|
358
|
-
#
|
|
359
|
-
# @param attrs [Hash] Event attributes
|
|
360
|
-
# @option attrs [Integer] :id User ID
|
|
361
|
-
# @option attrs [String] :email User email
|
|
362
|
-
# @option attrs [String] :name User name
|
|
363
|
-
# @option attrs [Hash] :payload Additional payload data
|
|
364
|
-
# @return [Models::Event] Event object
|
|
365
|
-
def self.user_created_event(attrs = {})
|
|
366
|
-
build_jetstream_event(
|
|
367
|
-
event_type: 'user.created',
|
|
368
|
-
payload: {
|
|
369
|
-
id: attrs[:id] || 1,
|
|
370
|
-
email: attrs[:email] || 'test@example.com',
|
|
371
|
-
name: attrs[:name] || 'Test User'
|
|
372
|
-
}.merge(attrs[:payload] || {})
|
|
373
|
-
)
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
# Build multiple sample events
|
|
377
|
-
#
|
|
378
|
-
# @param count [Integer] Number of events to create
|
|
379
|
-
# @param type [String] Event type
|
|
380
|
-
# @return [Array<Models::Event>] Array of event objects
|
|
381
|
-
def self.sample_events(count = 3, type: 'test.event')
|
|
382
|
-
Array.new(count) do |i|
|
|
383
|
-
build_jetstream_event(
|
|
384
|
-
event_type: type,
|
|
385
|
-
payload: { id: i + 1, sequence: i }
|
|
386
|
-
)
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
# Build a generic event with custom attributes
|
|
391
|
-
#
|
|
392
|
-
# @param event_type [String] Event type
|
|
393
|
-
# @param payload [Hash] Event payload
|
|
394
|
-
# @param attrs [Hash] Additional event attributes
|
|
395
|
-
# @return [Models::Event] Event object
|
|
396
|
-
def self.event(event_type:, payload: {}, **attrs)
|
|
397
|
-
build_jetstream_event(
|
|
398
|
-
event_type: event_type,
|
|
399
|
-
payload: payload,
|
|
400
|
-
**attrs
|
|
401
|
-
)
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
# Helper method to build events
|
|
405
|
-
# @private
|
|
406
|
-
def self.build_jetstream_event(event_type:, payload:, event_id: nil, trace_id: nil, occurred_at: nil, **metadata)
|
|
407
|
-
# Delegate to main module method to avoid duplication
|
|
408
|
-
TestHelpers.build_jetstream_event(
|
|
409
|
-
event_type: event_type,
|
|
410
|
-
payload: payload,
|
|
411
|
-
event_id: event_id,
|
|
412
|
-
trace_id: trace_id,
|
|
413
|
-
occurred_at: occurred_at,
|
|
414
|
-
**metadata
|
|
415
|
-
)
|
|
416
|
-
end
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
# Integration test helpers for end-to-end testing
|
|
420
|
-
module IntegrationHelpers
|
|
421
|
-
# Publish an event and wait for it to appear in mock storage
|
|
422
|
-
#
|
|
423
|
-
# @param event_attrs [Hash] Event attributes to publish
|
|
424
|
-
# @param timeout [Integer] Maximum seconds to wait
|
|
425
|
-
# @return [Models::PublishResult] Publish result
|
|
426
|
-
# @raise [Timeout::Error] If event doesn't appear within timeout
|
|
427
|
-
def publish_and_wait(event_attrs, timeout: 1)
|
|
428
|
-
result = JetstreamBridge.publish(**event_attrs)
|
|
429
|
-
|
|
430
|
-
deadline = Time.now + timeout
|
|
431
|
-
until Time.now > deadline
|
|
432
|
-
storage = JetstreamBridge::TestHelpers.mock_storage
|
|
433
|
-
break if storage.messages.any? { |m| m[:header]['nats-msg-id'] == result.event_id }
|
|
434
|
-
|
|
435
|
-
sleep 0.01
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
result
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
# Consume events from mock storage
|
|
442
|
-
#
|
|
443
|
-
# @param batch_size [Integer] Number of events to consume
|
|
444
|
-
# @yield [event] Block to handle each event
|
|
445
|
-
# @return [Array<Hash>] Consumed events
|
|
446
|
-
def consume_events(batch_size: 10, &handler)
|
|
447
|
-
storage = JetstreamBridge::TestHelpers.mock_storage
|
|
448
|
-
messages = storage.messages.first(batch_size)
|
|
449
|
-
|
|
450
|
-
messages.each do |msg|
|
|
451
|
-
event = JetstreamBridge::Models::Event.from_nats_message(
|
|
452
|
-
OpenStruct.new(
|
|
453
|
-
subject: msg[:subject],
|
|
454
|
-
data: msg[:data],
|
|
455
|
-
header: msg[:header],
|
|
456
|
-
metadata: OpenStruct.new(
|
|
457
|
-
sequence: OpenStruct.new(stream: msg[:sequence]),
|
|
458
|
-
num_delivered: msg[:delivery_count],
|
|
459
|
-
stream: 'test-stream',
|
|
460
|
-
consumer: 'test-consumer'
|
|
461
|
-
)
|
|
462
|
-
)
|
|
463
|
-
)
|
|
464
|
-
|
|
465
|
-
handler&.call(event)
|
|
466
|
-
JetstreamBridge::TestHelpers.record_consumed_event(event.to_h)
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
JetstreamBridge::TestHelpers.consumed_events
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
# Wait for a specific number of messages in mock storage
|
|
473
|
-
#
|
|
474
|
-
# @param count [Integer] Expected message count
|
|
475
|
-
# @param timeout [Integer] Maximum seconds to wait
|
|
476
|
-
# @return [Boolean] true if count reached, false if timeout
|
|
477
|
-
def wait_for_messages(count, timeout: 2)
|
|
478
|
-
deadline = Time.now + timeout
|
|
479
|
-
storage = JetstreamBridge::TestHelpers.mock_storage
|
|
480
|
-
|
|
481
|
-
until Time.now > deadline
|
|
482
|
-
return true if storage.messages.size >= count
|
|
483
|
-
|
|
484
|
-
sleep 0.01
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
false
|
|
488
|
-
end
|
|
489
|
-
end
|
|
490
235
|
end
|
|
491
236
|
end
|
|
492
237
|
|
|
@@ -79,7 +79,7 @@ module JetstreamBridge
|
|
|
79
79
|
def log_retention_mismatch(name, have:, want:)
|
|
80
80
|
Logging.warn(
|
|
81
81
|
"Stream #{name} retention mismatch (have=#{have.inspect}, want=#{want.inspect}). " \
|
|
82
|
-
|
|
82
|
+
'Retention is immutable; skipping retention change.',
|
|
83
83
|
tag: 'JetstreamBridge::Stream'
|
|
84
84
|
)
|
|
85
85
|
end
|