bug_bunny 4.13.0 → 4.16.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 +47 -0
- data/README.md +51 -4
- data/lib/bug_bunny/client.rb +37 -0
- data/lib/bug_bunny/configuration.rb +14 -0
- data/lib/bug_bunny/exception.rb +61 -0
- data/lib/bug_bunny/producer.rb +154 -7
- data/lib/bug_bunny/request.rb +5 -1
- data/lib/bug_bunny/session.rb +98 -3
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +73 -7
- data/skill/references/client-middleware.md +66 -15
- data/skill/references/errores.md +7 -0
- data/spec/integration/publisher_confirms_spec.rb +304 -0
- data/spec/spec_helper.rb +4 -1
- data/spec/support/integration_helper.rb +8 -1
- data/spec/unit/client_session_pool_spec.rb +72 -0
- data/spec/unit/configuration_spec.rb +12 -0
- data/spec/unit/producer_spec.rb +266 -0
- data/spec/unit/request_spec.rb +16 -0
- data/spec/unit/session_spec.rb +72 -0
- metadata +4 -3
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'support/integration_helper'
|
|
5
|
+
|
|
6
|
+
# Specs de integración para Publisher Confirms en modo :confirmed + mandatory.
|
|
7
|
+
# Verifican el flow end-to-end del bridge `basic.return` → `PublishUnroutable`
|
|
8
|
+
# contra un RabbitMQ real.
|
|
9
|
+
#
|
|
10
|
+
# Se skippean automáticamente si el broker no está disponible (ver
|
|
11
|
+
# `spec_helper.rb` → `before(:each, :integration)`).
|
|
12
|
+
RSpec.describe 'Publisher Confirms — return_raise', :integration do
|
|
13
|
+
let(:client) { BugBunny::Client.new(pool: TEST_POOL) }
|
|
14
|
+
|
|
15
|
+
# Exchange unbound: existe pero ninguna cola está bindeada a él.
|
|
16
|
+
# Cualquier publish con mandatory:true sobre este exchange retornará.
|
|
17
|
+
let(:unbound_exchange) { unique('unroutable_x') }
|
|
18
|
+
# Exchange con cola bindeada: publish con mandatory:true llega bien.
|
|
19
|
+
let(:routable_exchange) { unique('routable_x') }
|
|
20
|
+
|
|
21
|
+
# Declara el exchange sin bindings para asegurar que `basic.return` se dispare.
|
|
22
|
+
# Usa una conexión fresca para no contaminar el pool.
|
|
23
|
+
def declare_unbound_exchange!(name)
|
|
24
|
+
conn = BugBunny.create_connection
|
|
25
|
+
ch = conn.create_channel
|
|
26
|
+
ch.topic(name, BugBunny.configuration.exchange_options)
|
|
27
|
+
ch.close
|
|
28
|
+
conn.close
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
before do
|
|
32
|
+
declare_unbound_exchange!(unbound_exchange)
|
|
33
|
+
# Reset flag a default conocido por si algún spec previo lo cambió.
|
|
34
|
+
BugBunny.configuration.return_raise = true
|
|
35
|
+
BugBunny.configuration.on_return = nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
after do
|
|
39
|
+
BugBunny.configuration.return_raise = true
|
|
40
|
+
BugBunny.configuration.on_return = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe 'mandatory: true sobre exchange sin bindings' do
|
|
44
|
+
it 'levanta BugBunny::PublishUnroutable por default (return_raise=true)' do
|
|
45
|
+
expect {
|
|
46
|
+
client.publish('acct.unbound',
|
|
47
|
+
exchange: unbound_exchange,
|
|
48
|
+
exchange_type: 'topic',
|
|
49
|
+
confirmed: true,
|
|
50
|
+
mandatory: true,
|
|
51
|
+
body: { tenant: 42 })
|
|
52
|
+
}.to raise_error(BugBunny::PublishUnroutable) do |err|
|
|
53
|
+
expect(err.path).to eq('acct.unbound')
|
|
54
|
+
expect(err.exchange).to eq(unbound_exchange)
|
|
55
|
+
expect(err.routing_key).to eq('acct.unbound')
|
|
56
|
+
expect(err.reply_code).to eq(312)
|
|
57
|
+
expect(err.reply_text).to match(/NO_ROUTE/i)
|
|
58
|
+
expect(err.correlation_id).to be_a(String)
|
|
59
|
+
expect(err.correlation_id).not_to be_empty
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'NO levanta si el request override `return_raise: false`' do
|
|
64
|
+
result = client.publish('acct.unbound',
|
|
65
|
+
exchange: unbound_exchange,
|
|
66
|
+
exchange_type: 'topic',
|
|
67
|
+
confirmed: true,
|
|
68
|
+
mandatory: true,
|
|
69
|
+
return_raise: false,
|
|
70
|
+
body: { tenant: 42 })
|
|
71
|
+
|
|
72
|
+
expect(result).to eq('status' => 202, 'body' => nil)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'NO levanta si la config global tiene `return_raise = false`' do
|
|
76
|
+
BugBunny.configuration.return_raise = false
|
|
77
|
+
|
|
78
|
+
result = client.publish('acct.unbound',
|
|
79
|
+
exchange: unbound_exchange,
|
|
80
|
+
exchange_type: 'topic',
|
|
81
|
+
confirmed: true,
|
|
82
|
+
mandatory: true,
|
|
83
|
+
body: { tenant: 42 })
|
|
84
|
+
|
|
85
|
+
expect(result).to eq('status' => 202, 'body' => nil)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'invoca el callback global on_return antes de levantar' do
|
|
89
|
+
captured = nil
|
|
90
|
+
BugBunny.configuration.on_return = lambda { |return_info, _props, _body|
|
|
91
|
+
captured = { exchange: return_info.exchange, rk: return_info.routing_key }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
expect {
|
|
95
|
+
client.publish('acct.unbound',
|
|
96
|
+
exchange: unbound_exchange,
|
|
97
|
+
exchange_type: 'topic',
|
|
98
|
+
confirmed: true,
|
|
99
|
+
mandatory: true,
|
|
100
|
+
body: { tenant: 42 })
|
|
101
|
+
}.to raise_error(BugBunny::PublishUnroutable)
|
|
102
|
+
|
|
103
|
+
expect(captured).not_to be_nil
|
|
104
|
+
expect(captured[:exchange]).to eq(unbound_exchange)
|
|
105
|
+
expect(captured[:rk]).to eq('acct.unbound')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'levanta igual cuando el user_cb on_return explota' do
|
|
109
|
+
BugBunny.configuration.on_return = ->(_, _, _) { raise 'boom in user cb' }
|
|
110
|
+
|
|
111
|
+
expect {
|
|
112
|
+
client.publish('acct.unbound',
|
|
113
|
+
exchange: unbound_exchange,
|
|
114
|
+
exchange_type: 'topic',
|
|
115
|
+
confirmed: true,
|
|
116
|
+
mandatory: true,
|
|
117
|
+
body: { tenant: 42 })
|
|
118
|
+
}.to raise_error(BugBunny::PublishUnroutable)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'override per-request gana sobre config global = false' do
|
|
122
|
+
BugBunny.configuration.return_raise = false
|
|
123
|
+
|
|
124
|
+
expect {
|
|
125
|
+
client.publish('acct.unbound',
|
|
126
|
+
exchange: unbound_exchange,
|
|
127
|
+
exchange_type: 'topic',
|
|
128
|
+
confirmed: true,
|
|
129
|
+
mandatory: true,
|
|
130
|
+
return_raise: true,
|
|
131
|
+
body: { tenant: 42 })
|
|
132
|
+
}.to raise_error(BugBunny::PublishUnroutable)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe 'mandatory: true sobre exchange con binding (happy path)' do
|
|
137
|
+
# Declara queue exclusive + binding contra `routable_exchange` para que el
|
|
138
|
+
# publish sea ruteable. Exclusive evita la deprecación de transient_nonexcl_queues
|
|
139
|
+
# en versiones modernas de RabbitMQ.
|
|
140
|
+
def with_exclusive_binding(exchange:, routing_key:)
|
|
141
|
+
conn = BugBunny.create_connection
|
|
142
|
+
ch = conn.create_channel
|
|
143
|
+
x = ch.topic(exchange, BugBunny.configuration.exchange_options)
|
|
144
|
+
q = ch.queue('', exclusive: true, auto_delete: true)
|
|
145
|
+
q.bind(x, routing_key: routing_key)
|
|
146
|
+
yield
|
|
147
|
+
ensure
|
|
148
|
+
ch&.close
|
|
149
|
+
conn&.close
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'retorna 202 sin levantar — el mensaje rutea normal' do
|
|
153
|
+
with_exclusive_binding(exchange: routable_exchange, routing_key: 'acct.#') do
|
|
154
|
+
result = client.publish('acct.start',
|
|
155
|
+
exchange: routable_exchange,
|
|
156
|
+
exchange_type: 'topic',
|
|
157
|
+
confirmed: true,
|
|
158
|
+
mandatory: true,
|
|
159
|
+
body: { tenant: 99 })
|
|
160
|
+
|
|
161
|
+
expect(result).to eq('status' => 202, 'body' => nil)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
describe 'mandatory: false (flag inerte)' do
|
|
167
|
+
it 'no levanta aunque return_raise=true y la routing key no rutee a ninguna cola' do
|
|
168
|
+
result = client.publish('acct.unbound',
|
|
169
|
+
exchange: unbound_exchange,
|
|
170
|
+
exchange_type: 'topic',
|
|
171
|
+
confirmed: true,
|
|
172
|
+
mandatory: false,
|
|
173
|
+
return_raise: true,
|
|
174
|
+
body: { tenant: 1 })
|
|
175
|
+
|
|
176
|
+
expect(result).to eq('status' => 202, 'body' => nil)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
describe 'concurrencia multi-thread sobre el mismo client' do
|
|
181
|
+
# Verifica que la correlación por correlation_id aísla los outcomes:
|
|
182
|
+
# N threads publican simultáneamente sobre el mismo exchange unbound, cada uno
|
|
183
|
+
# debe recibir SU propio PublishUnroutable (no el de otro thread).
|
|
184
|
+
it 'cada caller recibe su propio raise sin contaminación cross-thread' do
|
|
185
|
+
threads = 8
|
|
186
|
+
results = Concurrent::Array.new
|
|
187
|
+
|
|
188
|
+
pool = Array.new(threads) do |i|
|
|
189
|
+
Thread.new do
|
|
190
|
+
rk = "thread.#{i}.unbound"
|
|
191
|
+
client.publish(rk,
|
|
192
|
+
exchange: unbound_exchange,
|
|
193
|
+
exchange_type: 'topic',
|
|
194
|
+
confirmed: true,
|
|
195
|
+
mandatory: true,
|
|
196
|
+
body: { tid: i })
|
|
197
|
+
results << { tid: i, raised: false }
|
|
198
|
+
rescue BugBunny::PublishUnroutable => e
|
|
199
|
+
results << { tid: i, raised: true, rk: e.routing_key, cid: e.correlation_id }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
pool.each(&:join)
|
|
204
|
+
|
|
205
|
+
expect(results.size).to eq(threads)
|
|
206
|
+
expect(results.all? { |r| r[:raised] }).to be(true), 'todos deberían haber raised'
|
|
207
|
+
expect(results.map { |r| r[:rk] }.sort).to eq((0...threads).map { |i| "thread.#{i}.unbound" }.sort)
|
|
208
|
+
# Todos los correlation_ids deben ser distintos (no hubo cross-thread leakage)
|
|
209
|
+
cids = results.map { |r| r[:cid] }
|
|
210
|
+
expect(cids.uniq.size).to eq(threads)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
describe 'aislamiento entre exchanges sobre el mismo channel' do
|
|
215
|
+
# Publish A sobre exchange unbound (debe raisear) y publish B sobre exchange routable
|
|
216
|
+
# (debe pasar). Validamos que el return de A no contamina B.
|
|
217
|
+
it 'return en exchange A no afecta publish concurrente a exchange B' do
|
|
218
|
+
bound_ex = unique('bound_b_x')
|
|
219
|
+
bound_q = unique('bound_b_q')
|
|
220
|
+
|
|
221
|
+
# Setup: exchange routable con queue exclusive bindeada
|
|
222
|
+
conn = BugBunny.create_connection
|
|
223
|
+
ch = conn.create_channel
|
|
224
|
+
x = ch.topic(bound_ex, BugBunny.configuration.exchange_options)
|
|
225
|
+
q = ch.queue('', exclusive: true, auto_delete: true)
|
|
226
|
+
q.bind(x, routing_key: '#')
|
|
227
|
+
|
|
228
|
+
results = Concurrent::Array.new
|
|
229
|
+
|
|
230
|
+
t_a = Thread.new do
|
|
231
|
+
client.publish('a.unbound',
|
|
232
|
+
exchange: unbound_exchange,
|
|
233
|
+
exchange_type: 'topic',
|
|
234
|
+
confirmed: true,
|
|
235
|
+
mandatory: true,
|
|
236
|
+
body: { side: 'A' })
|
|
237
|
+
results << { side: 'A', raised: false }
|
|
238
|
+
rescue BugBunny::PublishUnroutable
|
|
239
|
+
results << { side: 'A', raised: true }
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
t_b = Thread.new do
|
|
243
|
+
client.publish('b.routable',
|
|
244
|
+
exchange: bound_ex,
|
|
245
|
+
exchange_type: 'topic',
|
|
246
|
+
confirmed: true,
|
|
247
|
+
mandatory: true,
|
|
248
|
+
body: { side: 'B' })
|
|
249
|
+
results << { side: 'B', raised: false }
|
|
250
|
+
rescue BugBunny::PublishUnroutable
|
|
251
|
+
results << { side: 'B', raised: true }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
[t_a, t_b].each(&:join)
|
|
255
|
+
|
|
256
|
+
a = results.find { |r| r[:side] == 'A' }
|
|
257
|
+
b = results.find { |r| r[:side] == 'B' }
|
|
258
|
+
expect(a[:raised]).to be(true), 'A (unbound) debería haber raised'
|
|
259
|
+
expect(b[:raised]).to be(false), 'B (routable) NO debería haber raised'
|
|
260
|
+
ensure
|
|
261
|
+
ch&.close
|
|
262
|
+
conn&.close
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
describe 'no hay leak en el registry tras publishes seriales' do
|
|
267
|
+
# 30 publishes seriales (mix routable + unroutable). Tras todos, el registry
|
|
268
|
+
# @pending_returns debe estar en 0. Detecta entries colgadas por cleanup mal hecho.
|
|
269
|
+
it 'registry vuelve a 0 tras una serie de publishes' do
|
|
270
|
+
30.times do |i|
|
|
271
|
+
target = i.even? ? unbound_exchange : nil
|
|
272
|
+
if target
|
|
273
|
+
begin
|
|
274
|
+
client.publish("serial.#{i}",
|
|
275
|
+
exchange: target,
|
|
276
|
+
exchange_type: 'topic',
|
|
277
|
+
confirmed: true,
|
|
278
|
+
mandatory: true,
|
|
279
|
+
body: { i: i })
|
|
280
|
+
rescue BugBunny::PublishUnroutable
|
|
281
|
+
# esperado en routes no-ruteables
|
|
282
|
+
end
|
|
283
|
+
else
|
|
284
|
+
# publish sin mandatory para no triggerear return — no-op para el registry
|
|
285
|
+
client.publish("serial.#{i}",
|
|
286
|
+
exchange: unbound_exchange,
|
|
287
|
+
exchange_type: 'topic',
|
|
288
|
+
confirmed: true,
|
|
289
|
+
mandatory: false,
|
|
290
|
+
body: { i: i })
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Inspeccionar cada Session del pool — el registry debe estar vacío en todas.
|
|
295
|
+
total_pending = 0
|
|
296
|
+
TEST_POOL.with do |conn|
|
|
297
|
+
session = conn.instance_variable_get(:@_bug_bunny_session)
|
|
298
|
+
registry = session.instance_variable_get(:@pending_returns)
|
|
299
|
+
total_pending += registry.size
|
|
300
|
+
end
|
|
301
|
+
expect(total_pending).to eq(0)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -23,8 +23,11 @@ BugBunny.configure do |config|
|
|
|
23
23
|
config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
|
|
24
24
|
config.vhost = '/'
|
|
25
25
|
config.logger = Logger.new($stdout).tap { |l| l.level = Logger::WARN }
|
|
26
|
+
# exchange_options: explícito para que los specs declaren exchanges efímeros.
|
|
27
|
+
# queue_options: NO override — confiamos en `DEFAULT_QUEUE_OPTIONS` (durable shared,
|
|
28
|
+
# válido en RMQ 3.x y 4.x). Specs que necesitan colas efímeras usan
|
|
29
|
+
# `TEST_WORKER_QUEUE_OPTS` desde `spec/support/integration_helper.rb`.
|
|
26
30
|
config.exchange_options = { durable: false, auto_delete: true }
|
|
27
|
-
config.queue_options = { exclusive: false, durable: false, auto_delete: true }
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
TEST_POOL ||= ConnectionPool.new(size: 5, timeout: 5) { BugBunny.create_connection }
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require 'timeout'
|
|
4
4
|
|
|
5
|
+
# Queue options usados por los workers de integración. \`exclusive: true\` evita
|
|
6
|
+
# la deprecación de \`transient_nonexcl_queues\` en RabbitMQ 4.x — la queue queda
|
|
7
|
+
# ligada a la conexión del worker y desaparece automáticamente al cerrarla.
|
|
8
|
+
# Sobreescribe la cascada de \`BugBunny.configuration.queue_options\`.
|
|
9
|
+
TEST_WORKER_QUEUE_OPTS = { exclusive: true, durable: false, auto_delete: true }.freeze unless defined?(TEST_WORKER_QUEUE_OPTS)
|
|
10
|
+
|
|
5
11
|
# Helpers compartidos para specs de integración con RabbitMQ real.
|
|
6
12
|
# Incluido automáticamente en todos los specs marcados con :integration.
|
|
7
13
|
RSpec.shared_context 'integration helpers' do
|
|
@@ -31,6 +37,7 @@ RSpec.shared_context 'integration helpers' do
|
|
|
31
37
|
exchange_name: exchange,
|
|
32
38
|
exchange_type: exchange_type,
|
|
33
39
|
routing_key: routing_key,
|
|
40
|
+
queue_opts: TEST_WORKER_QUEUE_OPTS,
|
|
34
41
|
block: true
|
|
35
42
|
)
|
|
36
43
|
rescue StandardError => e
|
|
@@ -57,7 +64,7 @@ RSpec.shared_context 'integration helpers' do
|
|
|
57
64
|
worker_thread = Thread.new do
|
|
58
65
|
ch = conn.create_channel
|
|
59
66
|
x = ch.public_send(exchange_type, exchange, BugBunny.configuration.exchange_options)
|
|
60
|
-
q = ch.queue(queue,
|
|
67
|
+
q = ch.queue(queue, TEST_WORKER_QUEUE_OPTS)
|
|
61
68
|
q.bind(x, routing_key: routing_key)
|
|
62
69
|
q.subscribe(block: true) do |delivery, props, body|
|
|
63
70
|
messages << { body: body, routing_key: delivery.routing_key, headers: props.headers }
|
|
@@ -215,6 +215,78 @@ RSpec.describe BugBunny::Client, 'session pooling' do
|
|
|
215
215
|
end
|
|
216
216
|
end
|
|
217
217
|
|
|
218
|
+
describe 'warn_return_raise_misuse' do
|
|
219
|
+
let(:log_io) { StringIO.new }
|
|
220
|
+
|
|
221
|
+
before do
|
|
222
|
+
@prev_logger = BugBunny.configuration.logger
|
|
223
|
+
BugBunny.configuration.logger = Logger.new(log_io).tap { |l| l.level = Logger::WARN }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
after do
|
|
227
|
+
BugBunny.configuration.logger = @prev_logger
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def stub_producer_to_noop
|
|
231
|
+
allow_any_instance_of(BugBunny::Producer).to receive(:confirmed) { { 'status' => 202, 'body' => nil } }
|
|
232
|
+
allow_any_instance_of(BugBunny::Producer).to receive(:fire) { { 'status' => 202, 'body' => nil } }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it 'logea warning cuando return_raise:true se pasa sin confirmed' do
|
|
236
|
+
stub_producer_to_noop
|
|
237
|
+
client = described_class.new(pool: fake_pool(fake_conn))
|
|
238
|
+
|
|
239
|
+
client.publish('foo', exchange: 'x', exchange_type: 'direct',
|
|
240
|
+
return_raise: true, mandatory: true)
|
|
241
|
+
|
|
242
|
+
expect(log_io.string).to include('event=client.return_raise_ignored')
|
|
243
|
+
expect(log_io.string).to include('delivery_mode=publish')
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it 'logea warning cuando return_raise:true se pasa sin mandatory' do
|
|
247
|
+
stub_producer_to_noop
|
|
248
|
+
client = described_class.new(pool: fake_pool(fake_conn))
|
|
249
|
+
|
|
250
|
+
client.publish('foo', exchange: 'x', exchange_type: 'direct',
|
|
251
|
+
return_raise: true, confirmed: true)
|
|
252
|
+
|
|
253
|
+
expect(log_io.string).to include('event=client.return_raise_ignored')
|
|
254
|
+
expect(log_io.string).to include('mandatory=false')
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'NO logea warning cuando confirmed+mandatory se setean via block API' do
|
|
258
|
+
stub_producer_to_noop
|
|
259
|
+
client = described_class.new(pool: fake_pool(fake_conn))
|
|
260
|
+
|
|
261
|
+
client.publish('foo', exchange: 'x', exchange_type: 'direct', return_raise: true) do |req|
|
|
262
|
+
req.delivery_mode = :confirmed
|
|
263
|
+
req.mandatory = true
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
expect(log_io.string).not_to include('client.return_raise_ignored')
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'NO logea warning cuando return_raise no fue seteado per-request (deja el default global)' do
|
|
270
|
+
stub_producer_to_noop
|
|
271
|
+
client = described_class.new(pool: fake_pool(fake_conn))
|
|
272
|
+
|
|
273
|
+
# Default global es true, pero el caller no fue explícito → no warneamos
|
|
274
|
+
client.publish('foo', exchange: 'x', exchange_type: 'direct')
|
|
275
|
+
|
|
276
|
+
expect(log_io.string).not_to include('client.return_raise_ignored')
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it 'NO logea warning cuando confirmed+mandatory+return_raise:true coexisten' do
|
|
280
|
+
stub_producer_to_noop
|
|
281
|
+
client = described_class.new(pool: fake_pool(fake_conn))
|
|
282
|
+
|
|
283
|
+
client.publish('foo', exchange: 'x', exchange_type: 'direct',
|
|
284
|
+
confirmed: true, mandatory: true, return_raise: true)
|
|
285
|
+
|
|
286
|
+
expect(log_io.string).not_to include('client.return_raise_ignored')
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
218
290
|
describe 'Session no se cierra entre requests' do
|
|
219
291
|
it 'no invoca close en la Session al terminar el request' do
|
|
220
292
|
conn = fake_conn
|
|
@@ -174,6 +174,18 @@ RSpec.describe BugBunny::Configuration do
|
|
|
174
174
|
end
|
|
175
175
|
end
|
|
176
176
|
|
|
177
|
+
describe 'return_raise flag' do
|
|
178
|
+
it 'tiene default true (raise PublishUnroutable en basic.return)' do
|
|
179
|
+
expect(BugBunny::Configuration.new.return_raise).to be(true)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it 'acepta false para opt-out (modo legacy)' do
|
|
183
|
+
configure_with(return_raise: false)
|
|
184
|
+
|
|
185
|
+
expect(BugBunny.configuration.return_raise).to be(false)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
177
189
|
describe '.validate! directamente' do
|
|
178
190
|
it 'es invocable directamente sobre la instancia' do
|
|
179
191
|
config = BugBunny::Configuration.new
|