bug_bunny 3.1.0 → 3.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +182 -350
- data/lib/bug_bunny/client.rb +26 -11
- data/lib/bug_bunny/configuration.rb +28 -2
- data/lib/bug_bunny/consumer.rb +13 -14
- data/lib/bug_bunny/controller.rb +2 -2
- data/lib/bug_bunny/producer.rb +41 -32
- data/lib/bug_bunny/request.rb +14 -2
- data/lib/bug_bunny/resource.rb +135 -16
- data/lib/bug_bunny/session.rb +47 -18
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -1
- data/test/integration/infrastructure_test.rb +61 -0
- data/test/integration/manual_client_test.rb +203 -0
- data/test/test_helper.rb +96 -11
- metadata +18 -16
- data/bin_client.rb +0 -51
- data/bin_suite.rb +0 -106
- data/bin_worker.rb +0 -26
- data/test/integration/fire_and_forget_test.rb +0 -76
- data/test/integration/rpc_flow_test.rb +0 -78
- data/test/unit/configuration_test.rb +0 -40
- data/test/unit/consumer_test.rb +0 -44
- data/test/unit/controller_headers_test.rb +0 -38
- data/test/unit/hybrid_resource_test.rb +0 -60
- data/test/unit/middleware_test.rb +0 -61
- data/test/unit/resource_test.rb +0 -49
- data/test_controller.rb +0 -49
- data/test_helper.rb +0 -20
- data/test_resource.rb +0 -19
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../test_helper'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
module ManualTest
|
|
7
|
+
class EchoController < BugBunny::Controller
|
|
8
|
+
def index
|
|
9
|
+
render status: 200, json: {
|
|
10
|
+
received: params[:message],
|
|
11
|
+
type: headers[:type],
|
|
12
|
+
via: 'ManualTest::EchoController'
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class ManualClientTest < Minitest::Test
|
|
19
|
+
include IntegrationHelper
|
|
20
|
+
|
|
21
|
+
def setup
|
|
22
|
+
skip "RabbitMQ no disponible" unless IntegrationHelper.rabbitmq_available?
|
|
23
|
+
|
|
24
|
+
# 1. NOMBRES ÚNICOS: Evitamos colisiones entre tests de diferentes tipos (Direct/Fanout)
|
|
25
|
+
@queue = "test_manual_q_#{SecureRandom.hex(4)}"
|
|
26
|
+
@exchange = "test_manual_x_#{SecureRandom.hex(4)}"
|
|
27
|
+
|
|
28
|
+
@client = BugBunny::Client.new(pool: TEST_POOL)
|
|
29
|
+
BugBunny.configure { |c| c.controller_namespace = 'ManualTest' }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def teardown
|
|
33
|
+
BugBunny.configure { |c| c.controller_namespace = 'Rabbit::Controllers' }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# ==========================================
|
|
37
|
+
# GRUPO 1: PUBLICACIÓN ASÍNCRONA (PUBLISH)
|
|
38
|
+
# ==========================================
|
|
39
|
+
|
|
40
|
+
def test_publish_topic
|
|
41
|
+
puts "\n -> [Manual] Publish (Topic)..."
|
|
42
|
+
with_spy_worker(queue: @queue, exchange: @exchange, exchange_type: 'topic', routing_key: 'logs.#') do |messages|
|
|
43
|
+
|
|
44
|
+
@client.publish('logs.error', exchange: @exchange, exchange_type: 'topic', body: { a: 1 })
|
|
45
|
+
|
|
46
|
+
msg = wait_for_message(messages)
|
|
47
|
+
assert_equal 'logs.error', msg[:routing_key]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_publish_direct
|
|
52
|
+
puts " -> [Manual] Publish (Direct)..."
|
|
53
|
+
with_spy_worker(queue: @queue, exchange: @exchange, exchange_type: 'direct', routing_key: 'alert') do |messages|
|
|
54
|
+
|
|
55
|
+
@client.publish('alert', exchange: @exchange, exchange_type: 'direct', body: { a: 1 })
|
|
56
|
+
|
|
57
|
+
msg = wait_for_message(messages)
|
|
58
|
+
assert_equal 'alert', msg[:routing_key]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_publish_fanout
|
|
63
|
+
puts " -> [Manual] Publish (Fanout)..."
|
|
64
|
+
with_spy_worker(queue: @queue, exchange: @exchange, exchange_type: 'fanout', routing_key: '') do |messages|
|
|
65
|
+
|
|
66
|
+
@client.publish('ignored.key', exchange: @exchange, exchange_type: 'fanout', body: { a: 1 })
|
|
67
|
+
|
|
68
|
+
msg = wait_for_message(messages)
|
|
69
|
+
assert_equal 'ignored.key', msg[:routing_key]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ==========================================
|
|
74
|
+
# GRUPO 2: RPC SÍNCRONO (REQUEST)
|
|
75
|
+
# ==========================================
|
|
76
|
+
|
|
77
|
+
def test_request_topic
|
|
78
|
+
puts " -> [Manual] RPC (Topic)..."
|
|
79
|
+
with_running_worker(queue: @queue, exchange: @exchange, exchange_type: 'topic', routing_key: 'echo') do
|
|
80
|
+
|
|
81
|
+
response = @client.request('echo',
|
|
82
|
+
method: :get, exchange: @exchange, exchange_type: 'topic',
|
|
83
|
+
body: { message: 'topic_rpc' }
|
|
84
|
+
)
|
|
85
|
+
assert_equal 'topic_rpc', response['body']['received']
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_request_direct
|
|
90
|
+
puts " -> [Manual] RPC (Direct)..."
|
|
91
|
+
direct_key = 'rpc.direct'
|
|
92
|
+
|
|
93
|
+
with_running_worker(queue: @queue, exchange: @exchange, exchange_type: 'direct', routing_key: direct_key) do
|
|
94
|
+
|
|
95
|
+
response = @client.request('echo',
|
|
96
|
+
method: :get,
|
|
97
|
+
routing_key: direct_key,
|
|
98
|
+
exchange: @exchange,
|
|
99
|
+
exchange_type: 'direct',
|
|
100
|
+
body: { message: 'direct_rpc' }
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
assert_equal 200, response['status']
|
|
104
|
+
assert_equal 'direct_rpc', response['body']['received']
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_request_fanout
|
|
109
|
+
puts " -> [Manual] RPC (Fanout)..."
|
|
110
|
+
with_running_worker(queue: @queue, exchange: @exchange, exchange_type: 'fanout', routing_key: '') do
|
|
111
|
+
|
|
112
|
+
response = @client.request('echo',
|
|
113
|
+
method: :get,
|
|
114
|
+
routing_key: 'random.ignored',
|
|
115
|
+
exchange: @exchange,
|
|
116
|
+
exchange_type: 'fanout',
|
|
117
|
+
body: { message: 'fanout_rpc' }
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
assert_equal 200, response['status']
|
|
121
|
+
assert_equal 'fanout_rpc', response['body']['received']
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# ==========================================
|
|
126
|
+
# GRUPO 3: OPCIONES DE INFRAESTRUCTURA (CASCADA NIVEL 3)
|
|
127
|
+
# ==========================================
|
|
128
|
+
|
|
129
|
+
def test_publish_with_custom_exchange_options
|
|
130
|
+
puts " -> [Manual] Publish (Custom Options Nivel 3)..."
|
|
131
|
+
custom_exchange = "custom_opts_x_#{SecureRandom.hex(4)}"
|
|
132
|
+
|
|
133
|
+
# 1. Pre-creamos el exchange exigiendo que sea DURABLE (contrario a la config global)
|
|
134
|
+
conn = BugBunny.create_connection
|
|
135
|
+
ch = conn.create_channel
|
|
136
|
+
ch.topic(custom_exchange, durable: true, auto_delete: true)
|
|
137
|
+
|
|
138
|
+
begin
|
|
139
|
+
# 2. El cliente publica inyectando opciones dinámicas.
|
|
140
|
+
# Si esto no funcionara, RabbitMQ nos tiraría PRECONDITION_FAILED
|
|
141
|
+
# porque la configuración global (Nivel 2) dice durable: false.
|
|
142
|
+
@client.publish('logs',
|
|
143
|
+
exchange: custom_exchange,
|
|
144
|
+
exchange_type: 'topic',
|
|
145
|
+
exchange_options: { durable: true, auto_delete: true }, # Nivel 3 sobrescribe Nivel 2
|
|
146
|
+
body: { test: 'options' }
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Si la ejecución llega aquí, significa que la Cascada funcionó perfecto.
|
|
150
|
+
assert true
|
|
151
|
+
ensure
|
|
152
|
+
ch&.exchange_delete(custom_exchange) rescue nil
|
|
153
|
+
conn&.close
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_request_with_custom_exchange_options
|
|
158
|
+
puts " -> [Manual] RPC (Custom Options Nivel 3)..."
|
|
159
|
+
custom_exchange = "custom_opts_rpc_x_#{SecureRandom.hex(4)}"
|
|
160
|
+
|
|
161
|
+
# 1. Exigimos DURABLE
|
|
162
|
+
conn = BugBunny.create_connection
|
|
163
|
+
ch = conn.create_channel
|
|
164
|
+
ch.direct(custom_exchange, durable: true, auto_delete: true)
|
|
165
|
+
|
|
166
|
+
# 2. Levantamos un worker usando esas configuraciones nativas
|
|
167
|
+
worker_thread = Thread.new do
|
|
168
|
+
q = ch.queue('', exclusive: true)
|
|
169
|
+
q.bind(custom_exchange, routing_key: 'custom.rpc')
|
|
170
|
+
q.subscribe(block: true) do |delivery, props, _body|
|
|
171
|
+
# Respuesta manual
|
|
172
|
+
ch.default_exchange.publish('{"status":200, "body":"ok"}', routing_key: props.reply_to, correlation_id: props.correlation_id)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
sleep 0.5
|
|
176
|
+
|
|
177
|
+
begin
|
|
178
|
+
# 3. El cliente hace el request inyectando las opciones
|
|
179
|
+
response = @client.request('test',
|
|
180
|
+
exchange: custom_exchange,
|
|
181
|
+
exchange_type: 'direct',
|
|
182
|
+
routing_key: 'custom.rpc',
|
|
183
|
+
exchange_options: { durable: true, auto_delete: true }, # Nivel 3
|
|
184
|
+
body: { req: 'data' }
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
assert_equal 200, response['status']
|
|
188
|
+
assert_equal 'ok', response['body']
|
|
189
|
+
ensure
|
|
190
|
+
worker_thread&.kill
|
|
191
|
+
ch&.exchange_delete(custom_exchange) rescue nil
|
|
192
|
+
conn&.close
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
def wait_for_message(queue, timeout_sec = 2)
|
|
199
|
+
Timeout.timeout(timeout_sec) { queue.pop }
|
|
200
|
+
rescue Timeout::Error
|
|
201
|
+
flunk "Timeout: No llegó el mensaje al Worker en #{timeout_sec}s"
|
|
202
|
+
end
|
|
203
|
+
end
|
data/test/test_helper.rb
CHANGED
|
@@ -1,24 +1,109 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require 'bundler/setup'
|
|
3
|
-
|
|
4
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
5
|
-
require 'bug_bunny'
|
|
6
2
|
|
|
3
|
+
require 'bundler/setup'
|
|
7
4
|
require 'minitest/autorun'
|
|
8
|
-
require '
|
|
9
|
-
require '
|
|
5
|
+
# require 'minitest/reporters'
|
|
6
|
+
require 'bug_bunny'
|
|
7
|
+
require 'connection_pool'
|
|
8
|
+
require 'securerandom'
|
|
9
|
+
require 'socket'
|
|
10
10
|
|
|
11
11
|
BugBunny.configure do |config|
|
|
12
|
-
config.
|
|
13
|
-
config.
|
|
12
|
+
config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
|
|
13
|
+
config.username = ENV.fetch('RABBITMQ_USER', 'guest')
|
|
14
|
+
config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
|
|
15
|
+
config.vhost = '/'
|
|
16
|
+
config.logger = Logger.new($stdout)
|
|
17
|
+
config.logger.level = Logger::WARN
|
|
18
|
+
|
|
19
|
+
# ========================================================
|
|
20
|
+
# LA MAGIA DE LA CASCADA (Nivel 2: Configuración Global)
|
|
21
|
+
# ========================================================
|
|
22
|
+
# Para los tests, queremos que los exchanges y queues sean efímeros
|
|
23
|
+
config.exchange_options = { durable: false, auto_delete: true }
|
|
24
|
+
config.queue_options = { exclusive: false, durable: false, auto_delete: true }
|
|
14
25
|
end
|
|
15
26
|
|
|
16
|
-
|
|
27
|
+
TEST_POOL = ConnectionPool.new(size: 5, timeout: 5) { BugBunny.create_connection }
|
|
28
|
+
BugBunny::Resource.connection_pool = TEST_POOL
|
|
29
|
+
|
|
30
|
+
module IntegrationHelper
|
|
17
31
|
def self.rabbitmq_available?
|
|
18
|
-
socket = TCPSocket.new(
|
|
32
|
+
socket = TCPSocket.new(BugBunny.configuration.host, 5672)
|
|
19
33
|
socket.close
|
|
20
34
|
true
|
|
21
|
-
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
|
|
35
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
|
|
22
36
|
false
|
|
23
37
|
end
|
|
38
|
+
|
|
39
|
+
def with_running_worker(queue:, exchange:, exchange_type: 'topic', routing_key: '#')
|
|
40
|
+
conn = BugBunny.create_connection
|
|
41
|
+
|
|
42
|
+
worker_thread = Thread.new do
|
|
43
|
+
ch = conn.create_channel
|
|
44
|
+
|
|
45
|
+
x_opts = BugBunny.configuration.exchange_options || {}
|
|
46
|
+
q_opts = BugBunny.configuration.queue_options || {}
|
|
47
|
+
|
|
48
|
+
# FIX: Usamos la API de alto nivel (public_send) idéntica a BugBunny::Session
|
|
49
|
+
ch.public_send(exchange_type, exchange, x_opts)
|
|
50
|
+
ch.close
|
|
51
|
+
|
|
52
|
+
BugBunny::Consumer.subscribe(
|
|
53
|
+
connection: conn,
|
|
54
|
+
queue_name: queue,
|
|
55
|
+
exchange_name: exchange,
|
|
56
|
+
exchange_type: exchange_type,
|
|
57
|
+
routing_key: routing_key,
|
|
58
|
+
queue_opts: q_opts,
|
|
59
|
+
block: true
|
|
60
|
+
)
|
|
61
|
+
rescue => e
|
|
62
|
+
puts "❌ WORKER CRASHED: #{e.message}"
|
|
63
|
+
puts e.backtrace.join("\n")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sleep 0.5
|
|
67
|
+
yield
|
|
68
|
+
ensure
|
|
69
|
+
conn&.close
|
|
70
|
+
worker_thread&.kill
|
|
71
|
+
sleep 0.1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def with_spy_worker(queue:, exchange:, exchange_type: 'topic', routing_key: '#')
|
|
75
|
+
captured_messages = Thread::Queue.new
|
|
76
|
+
conn = BugBunny.create_connection
|
|
77
|
+
|
|
78
|
+
worker_thread = Thread.new do
|
|
79
|
+
ch = conn.create_channel
|
|
80
|
+
|
|
81
|
+
x_opts = BugBunny.configuration.exchange_options || {}
|
|
82
|
+
q_opts = BugBunny.configuration.queue_options || {}
|
|
83
|
+
|
|
84
|
+
# FIX DEFINITIVO: Ahora 'x' es un hermoso objeto Bunny::Exchange
|
|
85
|
+
# que la función .bind() entiende perfectamente.
|
|
86
|
+
x = ch.public_send(exchange_type, exchange, x_opts)
|
|
87
|
+
q = ch.queue(queue, q_opts)
|
|
88
|
+
|
|
89
|
+
# Bindeamos el objeto al queue
|
|
90
|
+
q.bind(x, routing_key: routing_key)
|
|
91
|
+
|
|
92
|
+
q.subscribe(block: true) do |delivery, props, body|
|
|
93
|
+
captured_messages << {
|
|
94
|
+
body: body,
|
|
95
|
+
routing_key: delivery.routing_key,
|
|
96
|
+
headers: props.headers
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
rescue => e
|
|
100
|
+
puts "SPY WORKER ERROR: #{e.message}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sleep 0.5
|
|
104
|
+
yield(captured_messages)
|
|
105
|
+
ensure
|
|
106
|
+
conn&.close
|
|
107
|
+
worker_thread&.kill
|
|
108
|
+
end
|
|
24
109
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bug_bunny
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.1.
|
|
4
|
+
version: 3.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -206,6 +206,20 @@ dependencies:
|
|
|
206
206
|
- - "~>"
|
|
207
207
|
- !ruby/object:Gem::Version
|
|
208
208
|
version: '2.0'
|
|
209
|
+
- !ruby/object:Gem::Dependency
|
|
210
|
+
name: minitest-reporters
|
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
|
212
|
+
requirements:
|
|
213
|
+
- - "~>"
|
|
214
|
+
- !ruby/object:Gem::Version
|
|
215
|
+
version: '1.6'
|
|
216
|
+
type: :development
|
|
217
|
+
prerelease: false
|
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
219
|
+
requirements:
|
|
220
|
+
- - "~>"
|
|
221
|
+
- !ruby/object:Gem::Version
|
|
222
|
+
version: '1.6'
|
|
209
223
|
description: BugBunny is a lightweight RPC framework for Ruby on Rails over RabbitMQ.
|
|
210
224
|
It simulates a RESTful architecture with an intelligent router, Active Record-like
|
|
211
225
|
resources, and middleware support.
|
|
@@ -218,9 +232,6 @@ files:
|
|
|
218
232
|
- CHANGELOG.md
|
|
219
233
|
- README.md
|
|
220
234
|
- Rakefile
|
|
221
|
-
- bin_client.rb
|
|
222
|
-
- bin_suite.rb
|
|
223
|
-
- bin_worker.rb
|
|
224
235
|
- initializer_example.rb
|
|
225
236
|
- lib/bug_bunny.rb
|
|
226
237
|
- lib/bug_bunny/client.rb
|
|
@@ -241,18 +252,9 @@ files:
|
|
|
241
252
|
- lib/generators/bug_bunny/install/install_generator.rb
|
|
242
253
|
- lib/generators/bug_bunny/install/templates/initializer.rb
|
|
243
254
|
- sig/bug_bunny.rbs
|
|
244
|
-
- test/integration/
|
|
245
|
-
- test/integration/
|
|
255
|
+
- test/integration/infrastructure_test.rb
|
|
256
|
+
- test/integration/manual_client_test.rb
|
|
246
257
|
- test/test_helper.rb
|
|
247
|
-
- test/unit/configuration_test.rb
|
|
248
|
-
- test/unit/consumer_test.rb
|
|
249
|
-
- test/unit/controller_headers_test.rb
|
|
250
|
-
- test/unit/hybrid_resource_test.rb
|
|
251
|
-
- test/unit/middleware_test.rb
|
|
252
|
-
- test/unit/resource_test.rb
|
|
253
|
-
- test_controller.rb
|
|
254
|
-
- test_helper.rb
|
|
255
|
-
- test_resource.rb
|
|
256
258
|
homepage: https://github.com/gedera/bug_bunny
|
|
257
259
|
licenses:
|
|
258
260
|
- MIT
|
data/bin_client.rb
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# bin_client.rb
|
|
2
|
-
require_relative 'lib/bug_bunny'
|
|
3
|
-
$stdout.sync = true # <--- Agrega esto
|
|
4
|
-
|
|
5
|
-
# 1. Configuración
|
|
6
|
-
BugBunny.configure do |config|
|
|
7
|
-
config.host = 'localhost'
|
|
8
|
-
config.username = 'wisproMQ'
|
|
9
|
-
config.password = 'wisproMQ'
|
|
10
|
-
config.vhost = 'sync.devel'
|
|
11
|
-
config.logger = Logger.new(STDOUT)
|
|
12
|
-
config.rpc_timeout = 5
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# 2. Pool
|
|
16
|
-
POOL = ConnectionPool.new(size: 2, timeout: 5) do
|
|
17
|
-
BugBunny.create_connection
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# 3. Cliente
|
|
21
|
-
client = BugBunny.new(pool: POOL)
|
|
22
|
-
|
|
23
|
-
# --- PRUEBA 1: Publish ---
|
|
24
|
-
puts "\n[1] Enviando mensaje asíncrono (Publish)..."
|
|
25
|
-
|
|
26
|
-
# AGREGADO: exchange_type: 'topic'
|
|
27
|
-
client.publish('test/ping', exchange: 'test_exchange', exchange_type: 'topic', routing_key: 'test.ping') do |req|
|
|
28
|
-
req.body = { msg: 'Hola, soy invisible' }
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
puts " -> Enviado."
|
|
32
|
-
sleep 1
|
|
33
|
-
|
|
34
|
-
# --- PRUEBA 2: RPC ---
|
|
35
|
-
puts "\n[2] Enviando petición síncrona (Request)..."
|
|
36
|
-
|
|
37
|
-
begin
|
|
38
|
-
# AGREGADO: exchange_type: 'topic'
|
|
39
|
-
response = client.request('test/123/ping', exchange: 'test_exchange', exchange_type: 'topic', routing_key: 'test.ping') do |req|
|
|
40
|
-
req.body = { data: 'Importante' }
|
|
41
|
-
req.timeout = 3
|
|
42
|
-
req.headers['X-Source'] = 'Terminal'
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
puts " -> ✅ RESPUESTA RECIBIDA:"
|
|
46
|
-
puts " Status: #{response['status']}"
|
|
47
|
-
puts " Body: #{response['body']}"
|
|
48
|
-
|
|
49
|
-
rescue BugBunny::RequestTimeout
|
|
50
|
-
puts " -> ❌ Error: Timeout esperando respuesta."
|
|
51
|
-
end
|
data/bin_suite.rb
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
# bin_suite.rb
|
|
2
|
-
require_relative 'test_helper'
|
|
3
|
-
require_relative 'test_resource' # Cargamos la clase TestUser
|
|
4
|
-
|
|
5
|
-
# Cliente "Raw" para pruebas manuales
|
|
6
|
-
raw_client = BugBunny.new(pool: TEST_POOL)
|
|
7
|
-
|
|
8
|
-
def assert(condition, msg)
|
|
9
|
-
if condition
|
|
10
|
-
puts " ✅ PASS: #{msg}"
|
|
11
|
-
else
|
|
12
|
-
puts " ❌ FAIL: #{msg}"
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
puts "\n🔎 --- INICIANDO SUITE DE PRUEBAS BUG BUNNY ---"
|
|
17
|
-
|
|
18
|
-
# ---------------------------------------------------------
|
|
19
|
-
# TEST 1: RPC Manual (Raw Client)
|
|
20
|
-
# ---------------------------------------------------------
|
|
21
|
-
puts "\n[1] Probando RPC Manual (Client#request)..."
|
|
22
|
-
begin
|
|
23
|
-
# Notar la routing key: test_user.ping
|
|
24
|
-
response = raw_client.request('test_user/ping', exchange: 'test_exchange', exchange_type: 'topic', routing_key: 'test_user.ping')
|
|
25
|
-
assert(response['body']['message'] == 'Pong!', "Respuesta RPC recibida correctamente")
|
|
26
|
-
rescue => e
|
|
27
|
-
assert(false, "Error RPC: #{e.class} - #{e.message}")
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# ---------------------------------------------------------
|
|
31
|
-
# TEST 2: Resource Finder (ORM)
|
|
32
|
-
# ---------------------------------------------------------
|
|
33
|
-
puts "\n[2] Probando BugBunny::Resource (Estilo Rails)..."
|
|
34
|
-
|
|
35
|
-
# YA NO NECESITAS with_scope
|
|
36
|
-
puts " -> Buscando usuario ID 123..."
|
|
37
|
-
user = TestUser.find(123)
|
|
38
|
-
|
|
39
|
-
if user
|
|
40
|
-
assert(user.is_a?(TestUser), "El objeto retornado es un TestUser")
|
|
41
|
-
assert(user.name == "Gabriel", "El nombre cargó correctamente")
|
|
42
|
-
assert(user.persisted?, "El objeto figura como persistido")
|
|
43
|
-
else
|
|
44
|
-
assert(false, "No se encontró el usuario (Check worker logs)")
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# ---------------------------------------------------------
|
|
48
|
-
# TEST 3: Resource Create (ORM)
|
|
49
|
-
# ---------------------------------------------------------
|
|
50
|
-
puts "\n[3] Probando Resource Creation..."
|
|
51
|
-
puts " -> Creando usuario nuevo..."
|
|
52
|
-
new_user = TestUser.create(name: "Nuevo User", email: "new@test.com")
|
|
53
|
-
if new_user.persisted?
|
|
54
|
-
assert(new_user.persisted?, "El usuario se guardó y recibió ID")
|
|
55
|
-
assert(new_user.id.present?, "Tiene ID asignado por el worker (#{new_user.id})")
|
|
56
|
-
else
|
|
57
|
-
assert(false, "Fallo al crear usuario: #{new_user.errors.full_messages}")
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# ---------------------------------------------------------
|
|
61
|
-
# TEST 4: Validaciones Locales
|
|
62
|
-
# ---------------------------------------------------------
|
|
63
|
-
puts "\n[4] Probando Validaciones Locales..."
|
|
64
|
-
invalid_user = TestUser.new(email: "sin_nombre@test.com")
|
|
65
|
-
assert(invalid_user.valid? == false, "Usuario sin nombre es inválido")
|
|
66
|
-
assert(invalid_user.errors[:name].any?, "Tiene error en el campo :name")
|
|
67
|
-
|
|
68
|
-
# ---------------------------------------------------------
|
|
69
|
-
# TEST 5: Probando Configuración Dinámica (.with)...
|
|
70
|
-
# ---------------------------------------------------------
|
|
71
|
-
puts "\n[5] Probando Configuración Dinámica (.with)..."
|
|
72
|
-
|
|
73
|
-
# Probamos cambiar el routing key prefix temporalmente
|
|
74
|
-
begin
|
|
75
|
-
# Forzamos una routing key que no existe
|
|
76
|
-
puts " -> Intentando ruta incorrecta (esperando timeout)..."
|
|
77
|
-
TestUser.with(routing_key: 'ruta.incorrecta').find(123)
|
|
78
|
-
assert(false, "Debería haber fallado por timeout")
|
|
79
|
-
rescue BugBunny::RequestTimeout, BugBunny::ClientError
|
|
80
|
-
# Nota: Dependiendo de tu config, puede dar Timeout o 501 si llega a un worker default
|
|
81
|
-
puts " ✅ PASS: El override funcionó (timeout o error esperado en ruta incorrecta)"
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Probamos que vuelve a la normalidad
|
|
85
|
-
user = TestUser.find(123)
|
|
86
|
-
assert(user.present?, " ✅ PASS: La configuración volvió a la normalidad")
|
|
87
|
-
|
|
88
|
-
# ---------------------------------------------------------
|
|
89
|
-
# TEST 6: Filtrado Complejo (Query String Nested - FIX Rack)
|
|
90
|
-
# ---------------------------------------------------------
|
|
91
|
-
puts "\n[6] Probando Resource.where con filtros anidados (Fix Rack)..."
|
|
92
|
-
|
|
93
|
-
begin
|
|
94
|
-
# Esto fallaba antes (generaba string feo en la URL: {:active=>true})
|
|
95
|
-
# Al usar Rack, esto genera: ?q[active]=true&q[roles][]=admin
|
|
96
|
-
# No necesitamos que el worker responda algo real, solo que el request SALGA sin explotar URI.
|
|
97
|
-
TestUser.where(q: { active: true, roles: ['admin'] })
|
|
98
|
-
puts " ✅ PASS: .where generó la query anidada correctamente sin errores de URI."
|
|
99
|
-
rescue URI::InvalidURIError => e
|
|
100
|
-
assert(false, "❌ FAIL: URI Inválida (El fix de Rack no funcionó): #{e.message}")
|
|
101
|
-
rescue => e
|
|
102
|
-
# Si falla por conexión o 404 está bien, lo importante es que no falle al serializar
|
|
103
|
-
puts " ✅ PASS: El request se envió correctamente (aunque el worker responda: #{e.class}). Serialización OK."
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
puts "\n🏁 SUITE FINALIZADA"
|
data/bin_worker.rb
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# bin_worker.rb
|
|
4
|
-
require 'bundler/setup'
|
|
5
|
-
require 'bug_bunny'
|
|
6
|
-
require_relative 'test_controller' # Cargamos los controladores
|
|
7
|
-
|
|
8
|
-
puts '🐰 WORKER INICIADO (Exchange: Topic)...'
|
|
9
|
-
|
|
10
|
-
# Configuración básica
|
|
11
|
-
BugBunny.configure do |config|
|
|
12
|
-
config.logger = Logger.new($stdout)
|
|
13
|
-
config.logger.level = Logger::DEBUG
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Iniciar el Consumidor
|
|
17
|
-
# Escucha en la cola 'bug_bunny_queue', atada al exchange 'test_exchange' con routing key '#' (todo)
|
|
18
|
-
connection = BugBunny.create_connection
|
|
19
|
-
|
|
20
|
-
BugBunny::Consumer.subscribe(
|
|
21
|
-
connection: connection,
|
|
22
|
-
exchange_name: 'test_exchange',
|
|
23
|
-
exchange_type: 'topic',
|
|
24
|
-
queue_name: 'bug_bunny_test_queue',
|
|
25
|
-
routing_key: '#' # Wildcard para recibir todo en este test
|
|
26
|
-
)
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative '../test_helper'
|
|
4
|
-
require 'connection_pool'
|
|
5
|
-
|
|
6
|
-
class FireAndForgetTest < Minitest::Test
|
|
7
|
-
def setup
|
|
8
|
-
skip "RabbitMQ no disponible" unless TestHelper.rabbitmq_available?
|
|
9
|
-
|
|
10
|
-
# 1. Configuración
|
|
11
|
-
BugBunny.configure do |c|
|
|
12
|
-
c.host = 'localhost'
|
|
13
|
-
c.username = 'wisproMQ'
|
|
14
|
-
c.password = 'wisproMQ'
|
|
15
|
-
c.port = 5672
|
|
16
|
-
c.logger = Logger.new(nil)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
@pool = ConnectionPool.new(size: 1, timeout: 5) { BugBunny.create_connection }
|
|
20
|
-
|
|
21
|
-
# 2. Infraestructura de Test
|
|
22
|
-
@queue_name = 'test_fire_queue'
|
|
23
|
-
@exchange_name = 'test_fire_exchange'
|
|
24
|
-
@routing_key = 'logs.error'
|
|
25
|
-
|
|
26
|
-
# Usamos una Queue de Ruby para pasar el mensaje del Consumer al Test
|
|
27
|
-
@message_bucket = Queue.new
|
|
28
|
-
|
|
29
|
-
# 3. Consumidor "Espía"
|
|
30
|
-
@conn_consumer = BugBunny.create_connection
|
|
31
|
-
@consumer_thread = Thread.new do
|
|
32
|
-
ch = @conn_consumer.create_channel
|
|
33
|
-
# IMPORTANTE: Aquí declaramos el exchange como TOPIC
|
|
34
|
-
x = ch.topic(@exchange_name)
|
|
35
|
-
q = ch.queue(@queue_name).bind(x, routing_key: @routing_key)
|
|
36
|
-
|
|
37
|
-
q.subscribe(block: true) do |delivery_info, properties, body|
|
|
38
|
-
@message_bucket << {
|
|
39
|
-
body: body,
|
|
40
|
-
routing_key: delivery_info.routing_key
|
|
41
|
-
}
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
sleep 0.5 # Wait boot
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def teardown
|
|
48
|
-
return unless @conn_consumer
|
|
49
|
-
@conn_consumer.close
|
|
50
|
-
@consumer_thread.kill
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def test_publish_directly
|
|
54
|
-
client = BugBunny::Client.new(pool: @pool)
|
|
55
|
-
payload = { system: 'payment', error: 'timeout' }
|
|
56
|
-
|
|
57
|
-
# 1. Disparamos (Fire)
|
|
58
|
-
client.publish(
|
|
59
|
-
'logs/error',
|
|
60
|
-
body: payload,
|
|
61
|
-
exchange: @exchange_name,
|
|
62
|
-
exchange_type: 'topic',
|
|
63
|
-
routing_key: @routing_key
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
# 2. Verificamos asíncronamente
|
|
67
|
-
received = nil
|
|
68
|
-
Timeout.timeout(2) do
|
|
69
|
-
received = @message_bucket.pop
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# 3. Aserciones
|
|
73
|
-
assert_equal payload.to_json, received[:body]
|
|
74
|
-
assert_equal @routing_key, received[:routing_key]
|
|
75
|
-
end
|
|
76
|
-
end
|