nats_pubsub 1.0.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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/exe/nats_pubsub +44 -0
  3. data/lib/generators/nats_pubsub/config/config_generator.rb +174 -0
  4. data/lib/generators/nats_pubsub/config/templates/env.example.tt +46 -0
  5. data/lib/generators/nats_pubsub/config/templates/nats_pubsub.rb.tt +105 -0
  6. data/lib/generators/nats_pubsub/initializer/initializer_generator.rb +36 -0
  7. data/lib/generators/nats_pubsub/initializer/templates/nats_pubsub.rb +27 -0
  8. data/lib/generators/nats_pubsub/install/install_generator.rb +75 -0
  9. data/lib/generators/nats_pubsub/migrations/migrations_generator.rb +74 -0
  10. data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_inbox.rb.erb +88 -0
  11. data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_outbox.rb.erb +81 -0
  12. data/lib/generators/nats_pubsub/subscriber/subscriber_generator.rb +139 -0
  13. data/lib/generators/nats_pubsub/subscriber/templates/subscriber.rb.tt +117 -0
  14. data/lib/generators/nats_pubsub/subscriber/templates/subscriber_spec.rb.tt +116 -0
  15. data/lib/generators/nats_pubsub/subscriber/templates/subscriber_test.rb.tt +117 -0
  16. data/lib/nats_pubsub/active_record/publishable.rb +192 -0
  17. data/lib/nats_pubsub/cli.rb +105 -0
  18. data/lib/nats_pubsub/core/base_repository.rb +73 -0
  19. data/lib/nats_pubsub/core/config.rb +152 -0
  20. data/lib/nats_pubsub/core/config_presets.rb +139 -0
  21. data/lib/nats_pubsub/core/connection.rb +103 -0
  22. data/lib/nats_pubsub/core/constants.rb +190 -0
  23. data/lib/nats_pubsub/core/duration.rb +113 -0
  24. data/lib/nats_pubsub/core/error_action.rb +288 -0
  25. data/lib/nats_pubsub/core/event.rb +275 -0
  26. data/lib/nats_pubsub/core/health_check.rb +470 -0
  27. data/lib/nats_pubsub/core/logging.rb +72 -0
  28. data/lib/nats_pubsub/core/message_context.rb +193 -0
  29. data/lib/nats_pubsub/core/presets.rb +222 -0
  30. data/lib/nats_pubsub/core/retry_strategy.rb +71 -0
  31. data/lib/nats_pubsub/core/structured_logger.rb +141 -0
  32. data/lib/nats_pubsub/core/subject.rb +185 -0
  33. data/lib/nats_pubsub/instrumentation.rb +327 -0
  34. data/lib/nats_pubsub/middleware/active_record.rb +18 -0
  35. data/lib/nats_pubsub/middleware/chain.rb +92 -0
  36. data/lib/nats_pubsub/middleware/logging.rb +48 -0
  37. data/lib/nats_pubsub/middleware/retry_logger.rb +24 -0
  38. data/lib/nats_pubsub/middleware/structured_logging.rb +57 -0
  39. data/lib/nats_pubsub/models/event_model.rb +73 -0
  40. data/lib/nats_pubsub/models/inbox_event.rb +109 -0
  41. data/lib/nats_pubsub/models/model_codec_setup.rb +61 -0
  42. data/lib/nats_pubsub/models/model_utils.rb +57 -0
  43. data/lib/nats_pubsub/models/outbox_event.rb +113 -0
  44. data/lib/nats_pubsub/publisher/envelope_builder.rb +99 -0
  45. data/lib/nats_pubsub/publisher/fluent_batch.rb +262 -0
  46. data/lib/nats_pubsub/publisher/outbox_publisher.rb +97 -0
  47. data/lib/nats_pubsub/publisher/outbox_repository.rb +117 -0
  48. data/lib/nats_pubsub/publisher/publish_argument_parser.rb +108 -0
  49. data/lib/nats_pubsub/publisher/publish_result.rb +149 -0
  50. data/lib/nats_pubsub/publisher/publisher.rb +156 -0
  51. data/lib/nats_pubsub/rails/health_endpoint.rb +239 -0
  52. data/lib/nats_pubsub/railtie.rb +52 -0
  53. data/lib/nats_pubsub/subscribers/dlq_handler.rb +69 -0
  54. data/lib/nats_pubsub/subscribers/error_context.rb +137 -0
  55. data/lib/nats_pubsub/subscribers/error_handler.rb +110 -0
  56. data/lib/nats_pubsub/subscribers/graceful_shutdown.rb +128 -0
  57. data/lib/nats_pubsub/subscribers/inbox/inbox_message.rb +79 -0
  58. data/lib/nats_pubsub/subscribers/inbox/inbox_processor.rb +53 -0
  59. data/lib/nats_pubsub/subscribers/inbox/inbox_repository.rb +74 -0
  60. data/lib/nats_pubsub/subscribers/message_context.rb +86 -0
  61. data/lib/nats_pubsub/subscribers/message_processor.rb +225 -0
  62. data/lib/nats_pubsub/subscribers/message_router.rb +77 -0
  63. data/lib/nats_pubsub/subscribers/pool.rb +166 -0
  64. data/lib/nats_pubsub/subscribers/registry.rb +114 -0
  65. data/lib/nats_pubsub/subscribers/subscriber.rb +186 -0
  66. data/lib/nats_pubsub/subscribers/subscription_manager.rb +206 -0
  67. data/lib/nats_pubsub/subscribers/worker.rb +152 -0
  68. data/lib/nats_pubsub/tasks/install.rake +10 -0
  69. data/lib/nats_pubsub/testing/helpers.rb +199 -0
  70. data/lib/nats_pubsub/testing/matchers.rb +208 -0
  71. data/lib/nats_pubsub/testing/test_harness.rb +250 -0
  72. data/lib/nats_pubsub/testing.rb +157 -0
  73. data/lib/nats_pubsub/topology/overlap_guard.rb +88 -0
  74. data/lib/nats_pubsub/topology/stream.rb +102 -0
  75. data/lib/nats_pubsub/topology/stream_support.rb +170 -0
  76. data/lib/nats_pubsub/topology/subject_matcher.rb +77 -0
  77. data/lib/nats_pubsub/topology/topology.rb +24 -0
  78. data/lib/nats_pubsub/version.rb +8 -0
  79. data/lib/nats_pubsub/web/views/dashboard.erb +55 -0
  80. data/lib/nats_pubsub/web/views/inbox_detail.erb +91 -0
  81. data/lib/nats_pubsub/web/views/inbox_list.erb +62 -0
  82. data/lib/nats_pubsub/web/views/layout.erb +68 -0
  83. data/lib/nats_pubsub/web/views/outbox_detail.erb +77 -0
  84. data/lib/nats_pubsub/web/views/outbox_list.erb +62 -0
  85. data/lib/nats_pubsub/web.rb +181 -0
  86. data/lib/nats_pubsub.rb +290 -0
  87. metadata +225 -0
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateNatsPubsubOutbox < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
+ # Disable DDL transaction for concurrent index creation
5
+ disable_ddl_transaction!
6
+
7
+ def up
8
+ # Idempotency check - safe to run multiple times
9
+ return if table_exists?(:nats_pubsub_outbox)
10
+
11
+ create_table :nats_pubsub_outbox do |t|
12
+ t.string :event_id, null: false
13
+ t.string :subject, null: false
14
+ t.jsonb :payload, null: false, default: {}
15
+ t.jsonb :headers, null: false, default: {}
16
+ t.string :status, null: false, default: 'pending' # pending|publishing|sent|failed
17
+ t.integer :attempts, null: false, default: 0
18
+ t.text :last_error
19
+ t.datetime :enqueued_at
20
+ t.datetime :sent_at
21
+ t.timestamps
22
+ end
23
+
24
+ # Add indexes concurrently to avoid table locks
25
+ add_index :nats_pubsub_outbox, :event_id,
26
+ unique: true,
27
+ algorithm: :concurrently,
28
+ if_not_exists: true
29
+
30
+ add_index :nats_pubsub_outbox, :status,
31
+ algorithm: :concurrently,
32
+ if_not_exists: true
33
+
34
+ # Composite index for common queries (status + created_at)
35
+ add_index :nats_pubsub_outbox, [:status, :created_at],
36
+ algorithm: :concurrently,
37
+ if_not_exists: true,
38
+ name: 'index_outbox_on_status_and_created'
39
+
40
+ # Composite index for status + enqueued_at queries
41
+ add_index :nats_pubsub_outbox, [:status, :enqueued_at],
42
+ algorithm: :concurrently,
43
+ if_not_exists: true,
44
+ name: 'index_outbox_on_status_and_enqueued'
45
+
46
+ # Partial index for sent events (for cleanup queries)
47
+ add_index :nats_pubsub_outbox, :sent_at,
48
+ where: "status = 'sent'",
49
+ algorithm: :concurrently,
50
+ if_not_exists: true,
51
+ name: 'index_outbox_sent_at'
52
+
53
+ # Partial index for stale publishing records
54
+ add_index :nats_pubsub_outbox, :updated_at,
55
+ where: "status = 'publishing'",
56
+ algorithm: :concurrently,
57
+ if_not_exists: true,
58
+ name: 'index_outbox_stale_publishing'
59
+
60
+ # GIN index for JSONB payload queries (PostgreSQL only)
61
+ if ActiveRecord::Base.connection.adapter_name.downcase.include?('postgres')
62
+ add_index :nats_pubsub_outbox, :payload,
63
+ using: :gin,
64
+ algorithm: :concurrently,
65
+ if_not_exists: true,
66
+ name: 'index_outbox_payload_gin'
67
+ end
68
+
69
+ # Database-level constraint for status values
70
+ execute <<-SQL
71
+ ALTER TABLE nats_pubsub_outbox
72
+ ADD CONSTRAINT check_outbox_status_values
73
+ CHECK (status IN ('pending', 'publishing', 'sent', 'failed'))
74
+ SQL
75
+ end
76
+
77
+ def down
78
+ # Safe rollback with checks
79
+ drop_table :nats_pubsub_outbox if table_exists?(:nats_pubsub_outbox)
80
+ end
81
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module NatsPubsub
6
+ module Generators
7
+ # Subscriber generator that creates a new NatsPubsub subscriber class
8
+ #
9
+ # Usage:
10
+ # rails generate nats_pubsub:subscriber NAME [topic1 topic2...] [options]
11
+ #
12
+ # Examples:
13
+ # rails generate nats_pubsub:subscriber UserNotification
14
+ # rails generate nats_pubsub:subscriber OrderProcessor orders.order
15
+ # rails generate nats_pubsub:subscriber EmailHandler notifications.email --wildcard
16
+ # rails generate nats_pubsub:subscriber AuditLogger --topics=audit.user audit.order
17
+ #
18
+ # Options:
19
+ # --topics=one two three Specify topics to subscribe to
20
+ # --wildcard Use wildcard subscription (topic.>)
21
+ # --skip-test Skip test file generation
22
+ #
23
+ # This will create:
24
+ # app/subscribers/user_notification_subscriber.rb
25
+ # spec/subscribers/user_notification_subscriber_spec.rb (if RSpec is detected)
26
+ #
27
+ # The generated subscriber will:
28
+ # - Include NatsPubsub::Subscriber module
29
+ # - Subscribe to specified topics
30
+ # - Implement handle method stub
31
+ # - Include error handling example
32
+ class SubscriberGenerator < Rails::Generators::NamedBase
33
+ source_root File.expand_path('templates', __dir__)
34
+ desc 'Creates a NatsPubsub subscriber class'
35
+
36
+ argument :topics_list, type: :array, default: [], banner: 'topic1 topic2...'
37
+
38
+ class_option :topics, type: :array, default: [],
39
+ desc: 'Topics to subscribe to (alternative to positional args)'
40
+ class_option :wildcard, type: :boolean, default: false,
41
+ desc: 'Use wildcard subscription (topic.>)'
42
+ class_option :skip_test, type: :boolean, default: false,
43
+ desc: 'Skip test file generation'
44
+
45
+ def create_subscriber_file
46
+ template 'subscriber.rb.tt', File.join('app/subscribers', class_path, "#{file_name}_subscriber.rb")
47
+ say_status :created, "app/subscribers/#{file_name}_subscriber.rb", :green
48
+ rescue StandardError => e
49
+ say_status :error, "Failed to create subscriber: #{e.message}", :red
50
+ raise
51
+ end
52
+
53
+ def create_test_file
54
+ return if options[:skip_test]
55
+
56
+ if rspec_detected?
57
+ create_rspec_file
58
+ elsif test_unit_detected?
59
+ create_test_unit_file
60
+ else
61
+ say_status :skipped, 'No test framework detected', :yellow
62
+ end
63
+ rescue StandardError => e
64
+ say_status :error, "Failed to create test: #{e.message}", :red
65
+ # Don't raise - test generation failure shouldn't stop subscriber creation
66
+ end
67
+
68
+ private
69
+
70
+ def create_rspec_file
71
+ template 'subscriber_spec.rb.tt',
72
+ File.join('spec/subscribers', class_path, "#{file_name}_subscriber_spec.rb")
73
+ say_status :created, "spec/subscribers/#{file_name}_subscriber_spec.rb", :green
74
+ end
75
+
76
+ def create_test_unit_file
77
+ template 'subscriber_test.rb.tt',
78
+ File.join('test/subscribers', class_path, "#{file_name}_subscriber_test.rb")
79
+ say_status :created, "test/subscribers/#{file_name}_subscriber_test.rb", :green
80
+ end
81
+
82
+ def rspec_detected?
83
+ File.exist?(File.join(destination_root, 'spec', 'spec_helper.rb')) ||
84
+ File.exist?(File.join(destination_root, 'spec', 'rails_helper.rb'))
85
+ end
86
+
87
+ def test_unit_detected?
88
+ File.exist?(File.join(destination_root, 'test', 'test_helper.rb'))
89
+ end
90
+
91
+ # Get all topics from both positional args and --topics option
92
+ def all_topics
93
+ combined = topics_list + options[:topics]
94
+ combined.empty? ? default_topics : combined.uniq
95
+ end
96
+
97
+ # Default topics based on subscriber name
98
+ def default_topics
99
+ [file_name.pluralize.tr('_', '.')]
100
+ end
101
+
102
+ # Check if using wildcard subscription
103
+ def use_wildcard?
104
+ options[:wildcard]
105
+ end
106
+
107
+ # Generate subscription code
108
+ def subscription_code
109
+ if all_topics.empty?
110
+ " # subscribe_to 'your.topic'\n # subscribe_to_wildcard 'your.topic'"
111
+ elsif use_wildcard?
112
+ all_topics.map { |topic| " subscribe_to_wildcard '#{topic}'" }.join("\n")
113
+ else
114
+ all_topics.map { |topic| " subscribe_to '#{topic}'" }.join("\n")
115
+ end
116
+ end
117
+
118
+ # Generate example topic for comments
119
+ def example_topic
120
+ all_topics.first || 'your.topic'
121
+ end
122
+
123
+ # Check if subscriber name ends with 'Subscriber'
124
+ def needs_subscriber_suffix?
125
+ !class_name.end_with?('Subscriber')
126
+ end
127
+
128
+ # Get correct class name with Subscriber suffix
129
+ def subscriber_class_name
130
+ needs_subscriber_suffix? ? "#{class_name}Subscriber" : class_name
131
+ end
132
+
133
+ # Get correct file name
134
+ def subscriber_file_name
135
+ needs_subscriber_suffix? ? "#{file_name}_subscriber" : file_name
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # <%= subscriber_class_name %> handles messages from NATS topics
4
+ #
5
+ # This subscriber listens to the following topic(s):
6
+ <% all_topics.each do |topic| -%>
7
+ # - <%= topic %><%= use_wildcard? ? '.>' : '' %>
8
+ <% end -%>
9
+ #
10
+ # Message format:
11
+ # {
12
+ # "event_id": "uuid",
13
+ # "domain": "domain_name",
14
+ # "resource": "resource_name",
15
+ # "action": "action_name",
16
+ # "data": { ... }
17
+ # }
18
+ #
19
+ # Context includes:
20
+ # - event_id: Unique event identifier
21
+ # - trace_id: Distributed tracing ID
22
+ # - deliveries: Number of delivery attempts
23
+ # - topic: The topic this message was published to
24
+ # - subject: Full NATS subject (env.app.domain.resource.action)
25
+ class <%= subscriber_class_name %>
26
+ include NatsPubsub::Subscriber
27
+
28
+ # Subscribe to topics
29
+ <%= subscription_code %>
30
+
31
+ # Configure JetStream options (optional)
32
+ # jetstream_options retry: 5, ack_wait: 30, max_deliver: 5
33
+
34
+ # Handle incoming messages
35
+ #
36
+ # @param message [Hash] The message payload
37
+ # @param context [NatsPubsub::Core::MessageContext] Message context
38
+ # @return [void]
39
+ #
40
+ # @example
41
+ # {
42
+ # "event_id" => "uuid",
43
+ # "domain" => "<%= example_topic.split('.').first %>",
44
+ # "resource" => "<%= example_topic.split('.')[1] || 'resource' %>",
45
+ # "action" => "created",
46
+ # "data" => { "id" => 1, "name" => "Example" }
47
+ # }
48
+ def handle(message, context)
49
+ logger.info "Processing message: event_id=#{context.event_id} topic=#{context.topic}"
50
+
51
+ # Extract data from message
52
+ data = message['data'] || message
53
+ event_id = message['event_id']
54
+ action = message['action']
55
+
56
+ # TODO: Implement your message processing logic here
57
+ # Example:
58
+ # case action
59
+ # when 'created'
60
+ # handle_created(data, context)
61
+ # when 'updated'
62
+ # handle_updated(data, context)
63
+ # when 'deleted'
64
+ # handle_deleted(data, context)
65
+ # else
66
+ # logger.warn "Unknown action: #{action}"
67
+ # end
68
+
69
+ logger.info "Successfully processed: event_id=#{context.event_id}"
70
+ rescue StandardError => e
71
+ logger.error "Failed to process message: #{e.class}: #{e.message}"
72
+ logger.error e.backtrace.join("\n")
73
+ raise # Re-raise to trigger retry/DLQ logic
74
+ end
75
+
76
+ # Optional: Custom error handling
77
+ #
78
+ # Return an error action to control how errors are handled:
79
+ # - NatsPubsub::Core::ErrorAction::RETRY - Retry the message (default)
80
+ # - NatsPubsub::Core::ErrorAction::DISCARD - Discard the message
81
+ # - NatsPubsub::Core::ErrorAction::DLQ - Send to dead letter queue
82
+ #
83
+ # @param error_context [NatsPubsub::Core::ErrorContext] Error context
84
+ # @return [Symbol] Error action
85
+ #
86
+ # @example
87
+ # def on_error(error_context)
88
+ # case error_context.error
89
+ # when ActiveRecord::RecordNotFound
90
+ # NatsPubsub::Core::ErrorAction::DISCARD
91
+ # when Timeout::Error
92
+ # NatsPubsub::Core::ErrorAction::RETRY
93
+ # else
94
+ # NatsPubsub::Core::ErrorAction::DLQ
95
+ # end
96
+ # end
97
+ # def on_error(error_context)
98
+ # super
99
+ # end
100
+
101
+ private
102
+
103
+ # Example helper methods for different actions
104
+ # Uncomment and implement as needed
105
+
106
+ # def handle_created(data, context)
107
+ # # Handle created events
108
+ # end
109
+
110
+ # def handle_updated(data, context)
111
+ # # Handle updated events
112
+ # end
113
+
114
+ # def handle_deleted(data, context)
115
+ # # Handle deleted events
116
+ # end
117
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe <%= subscriber_class_name %>, nats_fake: true do
6
+ subject(:subscriber) { described_class.new }
7
+
8
+ describe '.all_subscriptions' do
9
+ it 'subscribes to correct topics' do
10
+ <% all_topics.each do |topic| -%>
11
+ expect(described_class.all_subscriptions).to include(
12
+ hash_including(topic: '<%= topic %>')
13
+ )
14
+ <% end -%>
15
+ end
16
+ end
17
+
18
+ describe '#handle' do
19
+ let(:event_id) { SecureRandom.uuid }
20
+ let(:trace_id) { SecureRandom.uuid }
21
+ let(:context) do
22
+ NatsPubsub::Core::MessageContext.new(
23
+ event_id: event_id,
24
+ trace_id: trace_id,
25
+ topic: '<%= example_topic %>',
26
+ subject: 'test.app.<%= example_topic %>',
27
+ deliveries: 1,
28
+ timestamp: Time.current
29
+ )
30
+ end
31
+
32
+ context 'with valid message' do
33
+ let(:message) do
34
+ {
35
+ 'event_id' => event_id,
36
+ 'domain' => '<%= example_topic.split('.').first %>',
37
+ 'resource' => '<%= example_topic.split('.')[1] || 'resource' %>',
38
+ 'action' => 'created',
39
+ 'data' => {
40
+ 'id' => 1,
41
+ 'name' => 'Test <%= class_name %>'
42
+ }
43
+ }
44
+ end
45
+
46
+ it 'processes the message successfully' do
47
+ expect { subscriber.handle(message, context) }.not_to raise_error
48
+ end
49
+
50
+ it 'logs processing information' do
51
+ allow(Rails.logger).to receive(:info)
52
+
53
+ subscriber.handle(message, context)
54
+
55
+ expect(Rails.logger).to have_received(:info)
56
+ .with(/Processing message: event_id=#{event_id}/)
57
+ end
58
+
59
+ # TODO: Add specific assertions for your business logic
60
+ # it 'creates the expected record' do
61
+ # expect { subscriber.handle(message, context) }
62
+ # .to change { YourModel.count }.by(1)
63
+ # end
64
+ end
65
+
66
+ context 'with invalid message' do
67
+ let(:message) { {} }
68
+
69
+ it 'handles missing data gracefully' do
70
+ # TODO: Implement based on your error handling strategy
71
+ # expect { subscriber.handle(message, context) }.not_to raise_error
72
+ # OR
73
+ # expect { subscriber.handle(message, context) }.to raise_error(StandardError)
74
+ end
75
+ end
76
+
77
+ context 'when processing fails' do
78
+ let(:message) do
79
+ {
80
+ 'event_id' => event_id,
81
+ 'action' => 'created',
82
+ 'data' => { 'id' => 1 }
83
+ }
84
+ end
85
+
86
+ before do
87
+ # Simulate a processing failure
88
+ # allow(YourService).to receive(:call).and_raise(StandardError, 'Processing failed')
89
+ end
90
+
91
+ it 'logs the error' do
92
+ # TODO: Implement based on your error handling
93
+ # allow(Rails.logger).to receive(:error)
94
+ # expect { subscriber.handle(message, context) }.to raise_error(StandardError)
95
+ # expect(Rails.logger).to have_received(:error)
96
+ end
97
+ end
98
+ end
99
+
100
+ # Optional: Test custom error handling
101
+ # describe '#on_error' do
102
+ # let(:error_context) do
103
+ # NatsPubsub::Core::ErrorContext.new(
104
+ # error: StandardError.new('Test error'),
105
+ # message: {},
106
+ # context: double(event_id: 'test-id'),
107
+ # attempts: 1
108
+ # )
109
+ # end
110
+ #
111
+ # it 'returns appropriate error action' do
112
+ # result = subscriber.on_error(error_context)
113
+ # expect(result).to eq(NatsPubsub::Core::ErrorAction::RETRY)
114
+ # end
115
+ # end
116
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class <%= subscriber_class_name %>Test < ActiveSupport::TestCase
6
+ def setup
7
+ @subscriber = <%= subscriber_class_name %>.new
8
+ @event_id = SecureRandom.uuid
9
+ @trace_id = SecureRandom.uuid
10
+ @context = NatsPubsub::Core::MessageContext.new(
11
+ event_id: @event_id,
12
+ trace_id: @trace_id,
13
+ topic: '<%= example_topic %>',
14
+ subject: 'test.app.<%= example_topic %>',
15
+ deliveries: 1,
16
+ timestamp: Time.current
17
+ )
18
+
19
+ # Enable fake mode for testing
20
+ NatsPubsub::Testing.fake!
21
+ end
22
+
23
+ def teardown
24
+ NatsPubsub::Testing.clear!
25
+ end
26
+
27
+ test 'subscribes to correct topics' do
28
+ subscriptions = <%= subscriber_class_name %>.all_subscriptions
29
+ <% all_topics.each do |topic| -%>
30
+ assert_includes subscriptions.map { |s| s[:topic] }, '<%= topic %>'
31
+ <% end -%>
32
+ end
33
+
34
+ test 'processes valid message successfully' do
35
+ message = {
36
+ 'event_id' => @event_id,
37
+ 'domain' => '<%= example_topic.split('.').first %>',
38
+ 'resource' => '<%= example_topic.split('.')[1] || 'resource' %>',
39
+ 'action' => 'created',
40
+ 'data' => {
41
+ 'id' => 1,
42
+ 'name' => 'Test <%= class_name %>'
43
+ }
44
+ }
45
+
46
+ assert_nothing_raised do
47
+ @subscriber.handle(message, @context)
48
+ end
49
+ end
50
+
51
+ test 'logs processing information' do
52
+ message = {
53
+ 'event_id' => @event_id,
54
+ 'action' => 'created',
55
+ 'data' => { 'id' => 1 }
56
+ }
57
+
58
+ Rails.logger.expects(:info).with(regexp_matches(/Processing message/))
59
+ @subscriber.handle(message, @context)
60
+ end
61
+
62
+ # TODO: Add specific test cases for your business logic
63
+ # test 'creates expected record' do
64
+ # message = {
65
+ # 'event_id' => @event_id,
66
+ # 'action' => 'created',
67
+ # 'data' => { 'id' => 1, 'name' => 'Test' }
68
+ # }
69
+ #
70
+ # assert_difference 'YourModel.count', 1 do
71
+ # @subscriber.handle(message, @context)
72
+ # end
73
+ # end
74
+
75
+ test 'handles missing data' do
76
+ message = {}
77
+
78
+ # TODO: Implement based on your error handling strategy
79
+ # assert_nothing_raised do
80
+ # @subscriber.handle(message, @context)
81
+ # end
82
+ # OR
83
+ # assert_raises StandardError do
84
+ # @subscriber.handle(message, @context)
85
+ # end
86
+ end
87
+
88
+ test 'logs errors when processing fails' do
89
+ message = {
90
+ 'event_id' => @event_id,
91
+ 'action' => 'created',
92
+ 'data' => { 'id' => 1 }
93
+ }
94
+
95
+ # Simulate a processing failure
96
+ # YourService.stubs(:call).raises(StandardError, 'Processing failed')
97
+
98
+ # TODO: Implement based on your error handling
99
+ # Rails.logger.expects(:error).at_least_once
100
+ # assert_raises StandardError do
101
+ # @subscriber.handle(message, @context)
102
+ # end
103
+ end
104
+
105
+ # Optional: Test custom error handling
106
+ # test 'returns appropriate error action' do
107
+ # error_context = NatsPubsub::Core::ErrorContext.new(
108
+ # error: StandardError.new('Test error'),
109
+ # message: {},
110
+ # context: stub(event_id: 'test-id'),
111
+ # attempts: 1
112
+ # )
113
+ #
114
+ # result = @subscriber.on_error(error_context)
115
+ # assert_equal NatsPubsub::Core::ErrorAction::RETRY, result
116
+ # end
117
+ end