bug_bunny 4.12.0 → 4.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e38ff38727e34da253f669937054e58f4bda0d5a42ce270cc9ac1e7853d8feb
4
- data.tar.gz: 47b090304aaf7743addd2136c963d82ea8fd67bb82a17aad8958551047b79015
3
+ metadata.gz: d1bcda002a00d8ebc93786a496f8649d43e82d58645aa2b77e3d9b72044e7e07
4
+ data.tar.gz: b49f4c724ae39592d6c1a1774873fc8b272cf853688d998cbfb2f22d721d3589
5
5
  SHA512:
6
- metadata.gz: 1811143e41e170668c68f8a4b57721c4e9e6111145c253f62e09c5653027e75ee32d6432ac950a54b80a5e4f282ca1a64f184535935c5b1dad7f8df4bac12593
7
- data.tar.gz: b896459d25cea7925eaa3a010aa85cd5bcd302ab6f4551eb5b6d4cad554f4478050476e7cd9a4980590343d6a65e9822b10a7f864378117778b543f665ce9eb2
6
+ metadata.gz: 3b16b769d198b82f5455294bfca2c9c157f6dc7cb34a4cceedf9441d11677035a51c97b1d29e7cb59994d7d954072062919c24958ac340d9da018dded6c9e199
7
+ data.tar.gz: fdea4a6b6d49dd032787c520e5516045812d94afa5ba4dc8a6ff818e7e89bc6263f92877266e82a40a3789907a2aafd9727ecd2770949511b3e8584915cc87ab
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.13.0] - 2026-05-11
4
+
5
+ ### Nuevas funcionalidades
6
+ - **NACK explícito como excepción en modo `:confirmed` (#37):** `Producer#confirmed` ahora levanta `BugBunny::PublishNacked` cuando el broker NACKea la publicación, en lugar de retornar 202 silenciosamente. La excepción expone `path` y `nacked_count` para que callers críticos (auditoría, billing, RADIUS accounting) puedan escalar a HTTP 5xx y permitir retries upstream. Configurable globalmente vía `BugBunny.configuration.nack_raise` (default `true`) y por request via `client.publish(..., nack_raise: false)`. El evento `producer.confirms_nacked` se sigue logueando para observabilidad. — @Gabriel
7
+
8
+ ### Cambios de comportamiento (semi-breaking)
9
+ - **Default `nack_raise: true`:** Publicaciones con `confirmed: true` que reciben NACK del broker ahora levantan excepción por default. En 4.12.0, el NACK se logueaba pero retornaba 202 igualmente — comportamiento que ocultaba pérdida de mensajes desde la perspectiva del publisher. Para mantener el comportamiento previo: `BugBunny.configuration.nack_raise = false` o `nack_raise: false` per request.
10
+
3
11
  ## [4.12.0] - 2026-05-11
4
12
 
5
13
  ### Nuevas funcionalidades
data/README.md CHANGED
@@ -220,6 +220,9 @@ client.publish('acct.start',
220
220
  | `confirmed` | Boolean | `false` | Block until `wait_for_confirms` returns. |
221
221
  | `mandatory` | Boolean | `false` | Broker returns the message if it cannot be routed to any queue. Requires `confirmed: true` to be useful. |
222
222
  | `confirm_timeout` | Float | `nil` | Seconds to wait for the broker ACK. Raises `BugBunny::RequestTimeout` if exceeded. |
223
+ | `nack_raise` | Boolean | `nil` | Per-request override of `config.nack_raise`. When `nil`, falls back to the global flag. |
224
+
225
+ If the broker NACKs the publish (explicit rejection — disk full, internal confirm policy, etc.), the call raises `BugBunny::PublishNacked` by default. The exception exposes `path` and `nacked_count`. Critical publishers (audit, billing, RADIUS accounting) can let it bubble up to translate into HTTP 5xx so upstream systems retry. To restore the legacy "log-only" behaviour, set `BugBunny.configuration.nack_raise = false` globally or pass `nack_raise: false` per request.
223
226
 
224
227
  Unroutable returned messages are handled by a single global callback (see `config.on_return` below). The default handler logs them as `session.broker_return` at `warn` level — nothing is dropped silently.
225
228
 
@@ -93,9 +93,12 @@ module BugBunny
93
93
  # @option args [Boolean] :mandatory Si `true`, el broker retorna el mensaje si no es ruteable.
94
94
  # Para procesar retornos, configurar {BugBunny.configuration.on_return}.
95
95
  # @option args [Float] :confirm_timeout Segundos a esperar el confirm. `nil` espera indefinidamente.
96
+ # @option args [Boolean] :nack_raise Override per-request del flag
97
+ # `BugBunny.configuration.nack_raise`. Si `nil` (default), se usa la configuración global.
96
98
  # @yield [req] Bloque para configurar el objeto Request.
97
99
  # @return [Hash] `{ 'status' => 202, 'body' => nil }`.
98
100
  # @raise [BugBunny::RequestTimeout] Si `confirmed: true` y el broker no confirma a tiempo.
101
+ # @raise [BugBunny::PublishNacked] Si `confirmed: true`, el broker NACKea, y `nack_raise` resuelto es true.
99
102
  def publish(url, **args)
100
103
  send(url, **args) do |req|
101
104
  req.delivery_mode = args[:confirmed] ? :confirmed : :publish
@@ -175,6 +178,7 @@ module BugBunny
175
178
  def apply_publisher_confirms_args(req, args)
176
179
  req.mandatory = args[:mandatory] if args.key?(:mandatory)
177
180
  req.confirm_timeout = args[:confirm_timeout] if args.key?(:confirm_timeout)
181
+ req.nack_raise = args[:nack_raise] if args.key?(:nack_raise)
178
182
  end
179
183
 
180
184
  # Recupera o crea la Session asociada al slot de conexión dado.
@@ -153,6 +153,15 @@ module BugBunny
153
153
  # }
154
154
  attr_accessor :on_return
155
155
 
156
+ # @return [Boolean] Si `true` (default), {BugBunny::Producer#confirmed} levanta
157
+ # {BugBunny::PublishNacked} cuando el broker NACKea la publicación. Si `false`,
158
+ # el NACK solo se logea como `producer.confirms_nacked` y la llamada retorna
159
+ # `{ 'status' => 202 }` (modo legacy).
160
+ #
161
+ # El valor puede sobreescribirse por request pasando `nack_raise:` en
162
+ # `Client#publish`.
163
+ attr_accessor :nack_raise
164
+
156
165
  # @!endgroup
157
166
 
158
167
  # Inicializa la configuración con valores por defecto seguros.
@@ -194,9 +203,7 @@ module BugBunny
194
203
  @queue_options = {}
195
204
 
196
205
  @consumer_middlewares = ConsumerMiddleware::Stack.new
197
- @rpc_reply_headers = nil
198
- @on_rpc_reply = nil
199
- @on_return = nil
206
+ init_callback_defaults
200
207
  end
201
208
 
202
209
  # Construye la URL de conexión AMQP basada en los atributos configurados.
@@ -223,6 +230,17 @@ module BugBunny
223
230
 
224
231
  private
225
232
 
233
+ # Defaults para callbacks y flags relacionados con publish/RPC.
234
+ # Extraído de {#initialize} para mantener el ABC size dentro de los límites.
235
+ #
236
+ # @return [void]
237
+ def init_callback_defaults
238
+ @rpc_reply_headers = nil
239
+ @on_rpc_reply = nil
240
+ @on_return = nil
241
+ @nack_raise = true
242
+ end
243
+
226
244
  def validate_required!(attr, value, rules)
227
245
  return unless rules[:required]
228
246
  return unless value.nil? || (value.is_a?(String) && value.empty?)
@@ -19,6 +19,37 @@ module BugBunny
19
19
  # Protege contra vulnerabilidades de RCE validando la herencia de las clases enrutadas.
20
20
  class SecurityError < Error; end
21
21
 
22
+ # Error lanzado cuando el broker responde NACK a una publicación en modo `:confirmed`.
23
+ #
24
+ # Un NACK significa que el broker rechazó explícitamente el mensaje (ej: política de
25
+ # confirms interna, disk full, replicación insuficiente). El mensaje no fue aceptado
26
+ # y se considera no entregado — equivalente a un fallo de transporte desde la
27
+ # perspectiva del publisher.
28
+ #
29
+ # Se levanta por default desde {BugBunny::Producer#confirmed}. Para opt-out,
30
+ # configurar `BugBunny.configuration.nack_raise = false` o pasar
31
+ # `nack_raise: false` por request.
32
+ #
33
+ # @example
34
+ # rescue BugBunny::PublishNacked => e
35
+ # e.path # => 'acct.start'
36
+ # e.nacked_count # => 1
37
+ class PublishNacked < Error
38
+ # @return [String] La ruta del request cuyo publish fue NACKeado.
39
+ attr_reader :path
40
+
41
+ # @return [Integer] Cantidad de mensajes NACKeados según `Bunny::Channel#nacked_set`.
42
+ attr_reader :nacked_count
43
+
44
+ # @param path [String] Ruta lógica del request (ej: 'acct.start').
45
+ # @param nacked_count [Integer] Cantidad de NACKs reportados por el broker.
46
+ def initialize(path:, nacked_count:)
47
+ @path = path
48
+ @nacked_count = nacked_count
49
+ super("broker NACK on path=#{path} (nacked=#{nacked_count})")
50
+ end
51
+ end
52
+
22
53
  # ==========================================
23
54
  # Categoría: Errores del Cliente (4xx)
24
55
  # ==========================================
@@ -52,14 +52,17 @@ module BugBunny
52
52
  # A diferencia de {#rpc} (que espera la respuesta de un Consumer remoto), aquí solo se
53
53
  # espera el ACK del propio broker — no hay round-trip al servicio destino.
54
54
  #
55
- # @param request [BugBunny::Request] Request con `mandatory`, `confirm_timeout` y/o `on_return` opcionales.
55
+ # @param request [BugBunny::Request] Request con `mandatory`, `confirm_timeout`, `nack_raise`
56
+ # y/o `on_return` opcionales.
56
57
  # @return [Hash] `{ 'status' => 202, 'body' => nil }` si el broker confirmó la recepción.
57
58
  # @raise [BugBunny::RequestTimeout] Si el broker no confirma dentro de `confirm_timeout` segundos.
59
+ # @raise [BugBunny::PublishNacked] Si el broker NACKea la publicación y `nack_raise` está activo
60
+ # (default `true` — ver {BugBunny::Configuration#nack_raise}).
58
61
  # @raise [BugBunny::CommunicationError] Si el canal AMQP falla durante la publicación o confirm.
59
62
  def confirmed(request)
60
63
  publish_message(request)
61
- wait_for_confirms!(request)
62
- log_nacks_if_any(request)
64
+ acked = wait_for_confirms!(request)
65
+ handle_confirm_result(request, acked)
63
66
  { 'status' => 202, 'body' => nil }
64
67
  rescue BugBunny::Error
65
68
  raise
@@ -154,20 +157,44 @@ module BugBunny
154
157
  "Timeout (#{timeout}s) waiting for publisher confirms: #{request.path}"
155
158
  end
156
159
 
157
- # Logea las nack-eadas del canal si las hay.
158
- # NACK no es un error fatal: el broker rechazó rutear (ej. confirm policy interna),
159
- # pero el mensaje no se perdió silenciosamente queda en el set para auditoría.
160
+ # Procesa el resultado de `wait_for_confirms`.
161
+ #
162
+ # Si el broker NACKeó (`acked == false`), logea el evento `producer.confirms_nacked`
163
+ # y opcionalmente levanta {BugBunny::PublishNacked} según el flag `nack_raise`
164
+ # resuelto desde el request o la configuración global.
160
165
  #
161
166
  # @param request [BugBunny::Request]
167
+ # @param acked [Boolean] Resultado de `wait_for_confirms` (true = todos los confirms positivos).
168
+ # @raise [BugBunny::PublishNacked] Si hay NACK y `nack_raise?` es true.
162
169
  # @return [void]
163
- def log_nacks_if_any(request)
170
+ def handle_confirm_result(request, acked)
171
+ return if acked
172
+
173
+ count = nacked_count
174
+ safe_log(:warn, 'producer.confirms_nacked', count: count, path: request.path)
175
+ return unless nack_raise?(request)
176
+
177
+ raise BugBunny::PublishNacked.new(path: request.path, nacked_count: count)
178
+ end
179
+
180
+ # Cuenta los delivery tags NACKeados reportados por el canal.
181
+ #
182
+ # @return [Integer]
183
+ def nacked_count
164
184
  ch = @session.channel
165
- return unless ch.respond_to?(:nacked_set)
185
+ return 0 unless ch.respond_to?(:nacked_set)
186
+
187
+ ch.nacked_set&.size || 0
188
+ end
166
189
 
167
- nacked = ch.nacked_set
168
- return if nacked.nil? || nacked.empty?
190
+ # Resuelve el flag `nack_raise` con prioridad request > configuración global.
191
+ #
192
+ # @param request [BugBunny::Request]
193
+ # @return [Boolean]
194
+ def nack_raise?(request)
195
+ return request.nack_raise unless request.nack_raise.nil?
169
196
 
170
- safe_log(:warn, 'producer.confirms_nacked', count: nacked.size, path: request.path)
197
+ BugBunny.configuration.nack_raise
171
198
  end
172
199
 
173
200
  # Registra la petición en el log calculando las opciones de infraestructura.
@@ -27,6 +27,8 @@ module BugBunny
27
27
  # vía {BugBunny.configuration.on_return}.
28
28
  # @attr confirm_timeout [Float, nil] Segundos máximos a esperar el `wait_for_confirms`.
29
29
  # `nil` espera indefinidamente.
30
+ # @attr nack_raise [Boolean, nil] Override per-request del flag global
31
+ # `BugBunny.configuration.nack_raise`. `nil` (default) delega a la configuración global.
30
32
  class Request
31
33
  attr_accessor :body, :headers, :params, :path, :method, :exchange, :exchange_type, :routing_key, :timeout,
32
34
  :delivery_mode, :queue_options
@@ -35,7 +37,7 @@ module BugBunny
35
37
  attr_accessor :exchange_options
36
38
 
37
39
  # Publisher Confirms (delivery_mode = :confirmed)
38
- attr_accessor :mandatory, :confirm_timeout
40
+ attr_accessor :mandatory, :confirm_timeout, :nack_raise
39
41
 
40
42
  # Metadatos AMQP Estándar
41
43
  attr_accessor :app_id, :content_type, :content_encoding, :priority,
@@ -63,6 +65,7 @@ module BugBunny
63
65
  # Defaults para Publisher Confirms (modo :confirmed)
64
66
  @mandatory = false
65
67
  @confirm_timeout = nil
68
+ @nack_raise = nil
66
69
  end
67
70
 
68
71
  # Combina el path con los params como query string.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = '4.12.0'
4
+ VERSION = '4.13.0'
5
5
  end
data/skill/SKILL.md CHANGED
@@ -15,7 +15,7 @@ Skill de conocimiento completo sobre BugBunny. Consultame para cualquier pregunt
15
15
  **Session** — `BugBunny::Session` envuelve canales de Bunny con thread-safety y double-checked locking.
16
16
  **RPC** — Patrón síncrono que usa la pseudo-cola `amq.rabbitmq.reply-to` para respuestas sin crear queues temporales.
17
17
  **Fire-and-Forget** — Patrón asíncrono donde el producer publica y continúa sin esperar respuesta. Retorna `{ 'status' => 202 }`.
18
- **Publisher Confirms** — Extensión de RabbitMQ que confirma al publisher que el broker recibió el mensaje. BugBunny lo expone como `confirmed: true` en `Client#publish`: el publish bloquea hasta `wait_for_confirms`. NACK no es fatal; se logea pero el método retorna 202.
18
+ **Publisher Confirms** — Extensión de RabbitMQ que confirma al publisher que el broker recibió el mensaje. BugBunny lo expone como `confirmed: true` en `Client#publish`: el publish bloquea hasta `wait_for_confirms`. NACK del broker (rechazo explícito) levanta `BugBunny::PublishNacked` por default; configurable via `config.nack_raise = false` o `nack_raise: false` per request para volver al modo log-only.
19
19
  **Mandatory** — Flag de `basic.publish` que pide al broker retornar el mensaje al publisher si no es ruteable a ninguna cola. Se procesa via `basic.return` (no es respuesta del request).
20
20
  **basic.return** — Evento asincrónico que Bunny dispatcha por exchange (`Bunny::Exchange#on_return`). BugBunny registra un handler único por nombre de exchange al resolverlo en `Session#exchange` y lo delega a `Configuration#on_return` o al default que logea.
21
21
  **Resource** — ORM tipo ActiveRecord que mapea operaciones CRUD a llamadas AMQP.
@@ -202,7 +202,8 @@ client.publish('events', method: :post, body: { type: 'order.placed' })
202
202
  client.publish('acct.start', exchange: 'acct_x', body: payload,
203
203
  confirmed: true, mandatory: true, confirm_timeout: 0.5)
204
204
  # → { 'status' => 202, 'body' => nil } # broker ACK confirmado
205
- # Si broker NACK: logea `producer.confirms_nacked` y retorna 202 igual (NACK ≠ pérdida).
205
+ # Si broker NACK: logea `producer.confirms_nacked` y raise BugBunny::PublishNacked
206
+ # (default — opt-out con config.nack_raise = false o nack_raise: false per request).
206
207
  # Si timeout: raise BugBunny::RequestTimeout.
207
208
  # Si mandatory + no ruteable: dispara `Configuration#on_return` (default: logea `session.broker_return`).
208
209
  ```
@@ -46,6 +46,7 @@ end
46
46
  | `confirmed` | Boolean | `false` | En `Client#publish`, flipea `delivery_mode` a `:confirmed`. Bloquea hasta `wait_for_confirms`. |
47
47
  | `mandatory` | Boolean | `false` | Pide al broker retornar el mensaje si no es ruteable. Solo útil con `confirmed: true`. |
48
48
  | `confirm_timeout` | Float | `nil` | Segundos máximos a esperar el ACK. `nil` = espera indefinida. Excedido → `BugBunny::RequestTimeout`. |
49
+ | `nack_raise` | Boolean | `nil` | Override per-request de `config.nack_raise`. `nil` = usa flag global. |
49
50
 
50
51
  ## Producer (bajo nivel)
51
52
 
@@ -68,7 +69,7 @@ El `Producer` es usado internamente por el `Client`. Implementa tres patrones de
68
69
 
69
70
  - Publica y bloquea hasta `channel.wait_for_confirms` del broker.
70
71
  - Bunny 2.x **no soporta timeout** nativo en `wait_for_confirms` — BugBunny envuelve la llamada en un hilo auxiliar y usa `Concurrent::IVar#value(timeout)` como reloj. Si `confirm_timeout` expira → `BugBunny::RequestTimeout`.
71
- - NACK del broker no es fatal: se logea `producer.confirms_nacked` con `count` y `path`, y retorna 202 igual.
72
+ - Si `wait_for_confirms` devuelve `false` (broker NACKea), se logea `producer.confirms_nacked` con `count` y `path`. Por default (`config.nack_raise = true`) levanta `BugBunny::PublishNacked` con `path` y `nacked_count`. Para opt-out: `config.nack_raise = false` o pasar `nack_raise: false` per request — en ese caso solo logea y retorna 202.
72
73
  - Si `mandatory: true` y el mensaje no es ruteable, el broker dispara `basic.return`. El handler se atacha vía `Bunny::Exchange#on_return` en `Session#exchange` la primera vez que se resuelve cada exchange (cacheado por nombre, una sola vez por canal) y delega a `Configuration#on_return` o al logger por default.
73
74
  - Errores del canal se envuelven en `BugBunny::CommunicationError`; errores `BugBunny::Error` pre-existentes se propagan sin envolver.
74
75
 
@@ -164,6 +165,7 @@ req.timestamp # Time.now.to_i
164
165
  req.content_type # 'application/json'
165
166
  req.mandatory # Boolean — solo modo :confirmed
166
167
  req.confirm_timeout # Float|nil — solo modo :confirmed
168
+ req.nack_raise # Boolean|nil — override per-request de config.nack_raise (solo modo :confirmed)
167
169
  ```
168
170
 
169
171
  Cuando `mandatory == true`, `Request#amqp_options` inyecta `mandatory: true` en el hash que va a `basic_publish`.
@@ -182,7 +184,10 @@ Producer#confirmed
182
184
 
183
185
  ├──> wait_for_confirms! (espera ACK del broker, con timeout opcional)
184
186
 
185
- └──> log_nacks_if_any (si nacked_set no está vacío → log WARN)
187
+ └──> handle_confirm_result
188
+ ├─ acked == true → return { status: 202 }
189
+ └─ acked == false → log WARN producer.confirms_nacked
190
+ └─ raise BugBunny::PublishNacked (si config.nack_raise || req.nack_raise)
186
191
 
187
192
  Asíncronamente, si el broker no pudo rutear:
188
193
  broker ──basic.return──> Exchange#on_return ──> Session handler ──> Configuration#on_return
@@ -221,7 +226,7 @@ Excepciones del callback se capturan y se logean como `session.on_return_failed`
221
226
  | Auditoría, billing, eventos críticos | `:confirmed` (con `mandatory: true` si es ruteable) |
222
227
  | Request-response síncrono | `:rpc` |
223
228
 
224
- `:confirmed` cuesta un round-trip al broker pero **no** al consumer remoto — más rápido que RPC, con garantía de entrega al broker. NACK del broker es raro (típicamente por confirm policies internas) y no implica pérdida del mensaje.
229
+ `:confirmed` cuesta un round-trip al broker pero **no** al consumer remoto — más rápido que RPC, con garantía de entrega al broker. NACK del broker es raro (típicamente por confirm policies internas, disk full, replicación insuficiente) pero **sí** implica que el mensaje no fue aceptado. Por default BugBunny levanta `BugBunny::PublishNacked` para que el caller pueda escalar (ej: convertir a HTTP 503 y dejar que el sistema upstream reintente). Opt-out con `config.nack_raise = false` o `nack_raise: false` per request.
225
230
 
226
231
  ## Cascada de Configuración (3 niveles)
227
232
 
@@ -8,6 +8,7 @@ StandardError
8
8
  ├── BugBunny::CommunicationError
9
9
  ├── BugBunny::ConfigurationError
10
10
  ├── BugBunny::SecurityError
11
+ ├── BugBunny::PublishNacked
11
12
  ├── BugBunny::ClientError (4xx)
12
13
  │ ├── BugBunny::BadRequest (400)
13
14
  │ ├── BugBunny::NotFound (404)
@@ -37,6 +38,12 @@ StandardError
37
38
  **Cuándo:** El consumer resuelve la clase pero falla la validación `is_a?(BugBunny::Controller)`.
38
39
  **Resolución:** Verificar que el controlador herede de `BugBunny::Controller` y que `config.controller_namespace` sea correcto.
39
40
 
41
+ ### BugBunny::PublishNacked
42
+ **Causa:** El broker NACKea explícitamente una publicación en modo `:confirmed` (`Client#publish(..., confirmed: true)`). Indica que el mensaje no fue aceptado — disk full, replicación insuficiente, confirm policy interna, etc.
43
+ **Cuándo:** `Producer#confirmed` detecta `wait_for_confirms == false`. Se levanta por default (`config.nack_raise = true`).
44
+ **Atributos:** `path` (ruta del request) y `nacked_count` (cantidad de delivery tags NACKeados).
45
+ **Resolución:** Para casos críticos (auditoría, billing, RADIUS accounting), dejar que la excepción bubble para que el caller upstream reintente (ej: HTTP 503 → retry). Para tolerar NACKs (eventos best-effort), `BugBunny.configuration.nack_raise = false` global o `nack_raise: false` per request — en ese caso solo se logea `producer.confirms_nacked`.
46
+
40
47
  ## Errores de Cliente (4xx)
41
48
 
42
49
  ### BugBunny::BadRequest (400)
@@ -162,6 +162,18 @@ RSpec.describe BugBunny::Configuration do
162
162
  end
163
163
  end
164
164
 
165
+ describe 'nack_raise flag' do
166
+ it 'tiene default true (raise PublishNacked en NACK)' do
167
+ expect(BugBunny::Configuration.new.nack_raise).to be(true)
168
+ end
169
+
170
+ it 'acepta false para opt-out (modo legacy)' do
171
+ configure_with(nack_raise: false)
172
+
173
+ expect(BugBunny.configuration.nack_raise).to be(false)
174
+ end
175
+ end
176
+
165
177
  describe '.validate! directamente' do
166
178
  it 'es invocable directamente sobre la instancia' do
167
179
  config = BugBunny::Configuration.new
@@ -245,21 +245,59 @@ RSpec.describe BugBunny::Producer do
245
245
  )
246
246
  end
247
247
 
248
- it 'logea producer.confirms_nacked cuando hay nacks' do
249
- allow(mock_channel).to receive(:nacked_set).and_return(Set.new([1, 2]))
250
-
248
+ it 'NO logea producer.confirms_nacked cuando wait_for_confirms devuelve true' do
251
249
  confirmed_producer.confirmed(build_request)
252
250
 
253
251
  nack_event = logged_events.find { |e| e[:event] == 'producer.confirms_nacked' }
254
- expect(nack_event).not_to be_nil
255
- expect(nack_event[:kwargs]).to include(count: 2, path: 'acct.start')
252
+ expect(nack_event).to be_nil
256
253
  end
257
254
 
258
- it 'NO logea producer.confirms_nacked cuando nacked_set está vacío' do
259
- confirmed_producer.confirmed(build_request)
255
+ context 'cuando el broker NACKea (wait_for_confirms devuelve false)' do
256
+ before do
257
+ allow(mock_channel).to receive(:wait_for_confirms).and_return(false)
258
+ allow(mock_channel).to receive(:nacked_set).and_return(Set.new([1, 2]))
259
+ end
260
260
 
261
- nack_event = logged_events.find { |e| e[:event] == 'producer.confirms_nacked' }
262
- expect(nack_event).to be_nil
261
+ it 'levanta BugBunny::PublishNacked por default (config.nack_raise = true)' do
262
+ expect { confirmed_producer.confirmed(build_request) }.to raise_error(BugBunny::PublishNacked) do |err|
263
+ expect(err.path).to eq('acct.start')
264
+ expect(err.nacked_count).to eq(2)
265
+ end
266
+ end
267
+
268
+ it 'logea producer.confirms_nacked antes de levantar' do
269
+ expect { confirmed_producer.confirmed(build_request) }.to raise_error(BugBunny::PublishNacked)
270
+
271
+ nack_event = logged_events.find { |e| e[:event] == 'producer.confirms_nacked' }
272
+ expect(nack_event).not_to be_nil
273
+ expect(nack_event[:kwargs]).to include(count: 2, path: 'acct.start')
274
+ end
275
+
276
+ it 'no levanta si el request override `nack_raise = false`' do
277
+ req = build_request
278
+ req.nack_raise = false
279
+
280
+ result = confirmed_producer.confirmed(req)
281
+
282
+ expect(result).to eq('status' => 202, 'body' => nil)
283
+ expect(logged_events.find { |e| e[:event] == 'producer.confirms_nacked' }).not_to be_nil
284
+ end
285
+
286
+ it 'no levanta si la configuración global tiene `nack_raise = false`' do
287
+ allow(BugBunny.configuration).to receive(:nack_raise).and_return(false)
288
+
289
+ result = confirmed_producer.confirmed(build_request)
290
+
291
+ expect(result).to eq('status' => 202, 'body' => nil)
292
+ end
293
+
294
+ it 'el override per-request gana sobre la configuración global' do
295
+ allow(BugBunny.configuration).to receive(:nack_raise).and_return(false)
296
+ req = build_request
297
+ req.nack_raise = true
298
+
299
+ expect { confirmed_producer.confirmed(req) }.to raise_error(BugBunny::PublishNacked)
300
+ end
263
301
  end
264
302
 
265
303
  it 'levanta BugBunny::RequestTimeout si wait_for_confirms excede confirm_timeout' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bug_bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.12.0
4
+ version: 4.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix
@@ -303,7 +303,7 @@ metadata:
303
303
  homepage_uri: https://github.com/gedera/bug_bunny
304
304
  source_code_uri: https://github.com/gedera/bug_bunny
305
305
  changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
306
- documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.12.0/skill
306
+ documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.13.0/skill
307
307
  post_install_message:
308
308
  rdoc_options: []
309
309
  require_paths: