bug_bunny 4.9.1 → 4.10.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 +5 -0
- data/lib/bug_bunny/consumer.rb +25 -12
- data/lib/bug_bunny/controller.rb +6 -1
- data/lib/bug_bunny/middleware/raise_error.rb +8 -0
- data/lib/bug_bunny/producer.rb +9 -2
- data/lib/bug_bunny/remote_error.rb +55 -0
- data/lib/bug_bunny/resource.rb +12 -0
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -0
- data/skill/SKILL.md +4 -0
- data/skill/references/consumer.md +8 -7
- data/skills.lock +1 -1
- data/spec/integration/resource_spec.rb +44 -0
- data/spec/unit/route_spec.rb +180 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e1a5c8c069bdf7634c57b1df16aa226d5abc8fd6e864596d273431a680a1b55
|
|
4
|
+
data.tar.gz: 49b7cf39e53fe8c3f3b90331cfc4f171c8769264356ba1c1d4a767bf27b868ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '082acc0c03183a7d077381563ab9da50657e7468006334b5cfc2d00c8442d603285d38d0f557d67793c833fd6c4d5133c82b3cbbe5048f7f89721c19082971b9'
|
|
7
|
+
data.tar.gz: 9ee3d5a73a1e339d13241093bedfc9dfe212e2a77fb655f3fe21f1378b02d5e10c86417e12fe637eaa1f9591be3ae1e7c2a72d6ed3dac5ee3007f05d1ee7159f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.10.1] - 2026-04-08
|
|
4
|
+
|
|
5
|
+
### Correcciones
|
|
6
|
+
- Corregir route matching 404: el path del cliente ahora se normaliza antes de pasarlo a `RouteSet#recognize`. Antes, `URI.parse("http://dummy/#{path}")` prependead un `/` extra al path, causando que rutas existentes no hicieran match. Ahora se usa `path.gsub(%r{^/|/$}, '')` antes del recognize. — @Gabriel
|
|
7
|
+
|
|
3
8
|
## [4.9.1] - 2026-04-06
|
|
4
9
|
|
|
5
10
|
### Correcciones
|
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -176,23 +176,25 @@ module BugBunny
|
|
|
176
176
|
|
|
177
177
|
safe_log(:info, 'consumer.message_received', method: http_method, path: path,
|
|
178
178
|
routing_key: delivery_info.routing_key, **otel_fields)
|
|
179
|
-
safe_log(:
|
|
179
|
+
safe_log(:info, 'consumer.message_received_body', body: body&.truncate(500), body_size: body&.size || 0)
|
|
180
180
|
|
|
181
181
|
# ===================================================================
|
|
182
182
|
# 3. Ruteo Declarativo
|
|
183
183
|
# ===================================================================
|
|
184
|
-
|
|
184
|
+
normalized_path = path.gsub(%r{^/|/$}, '')
|
|
185
|
+
|
|
186
|
+
uri = URI.parse("http://dummy/#{normalized_path}")
|
|
185
187
|
|
|
186
188
|
# Extraemos query params (ej. /nodes?status=active)
|
|
187
189
|
query_params = uri.query ? Rack::Utils.parse_nested_query(uri.query) : {}
|
|
188
190
|
query_params = query_params.with_indifferent_access if defined?(ActiveSupport::HashWithIndifferentAccess)
|
|
189
191
|
|
|
190
192
|
# Le preguntamos al motor de rutas global quién debe manejar esto
|
|
191
|
-
route_info = BugBunny.routes.recognize(http_method,
|
|
193
|
+
route_info = BugBunny.routes.recognize(http_method, normalized_path)
|
|
192
194
|
|
|
193
195
|
if route_info.nil?
|
|
194
|
-
safe_log(:warn, 'consumer.route_not_found', method: http_method, path:
|
|
195
|
-
handle_fatal_error(properties, 404, 'Not Found', "No route matches [#{http_method}] \"
|
|
196
|
+
safe_log(:warn, 'consumer.route_not_found', method: http_method, path: normalized_path)
|
|
197
|
+
handle_fatal_error(properties, 404, 'Not Found', "No route matches [#{http_method}] \"#{normalized_path}\"")
|
|
196
198
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
197
199
|
return
|
|
198
200
|
end
|
|
@@ -256,7 +258,7 @@ module BugBunny
|
|
|
256
258
|
rescue StandardError => e
|
|
257
259
|
safe_log(:error, 'consumer.execution_error', duration_s: duration_s(start_time), **exception_metadata(e))
|
|
258
260
|
safe_log(:debug, 'consumer.execution_error_backtrace', backtrace: e.backtrace.first(5).join(' | '))
|
|
259
|
-
handle_fatal_error(properties, 500, 'Internal Server Error', e.message)
|
|
261
|
+
handle_fatal_error(properties, 500, 'Internal Server Error', e.message, e)
|
|
260
262
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
261
263
|
end
|
|
262
264
|
|
|
@@ -267,7 +269,12 @@ module BugBunny
|
|
|
267
269
|
# @param correlation_id [String] ID para correlacionar la respuesta con la petición original.
|
|
268
270
|
# @return [void]
|
|
269
271
|
def reply(payload, reply_to, correlation_id)
|
|
270
|
-
safe_log(:
|
|
272
|
+
safe_log(:info, 'consumer.rpc_reply',
|
|
273
|
+
reply_to: reply_to,
|
|
274
|
+
messaging_message_id: correlation_id,
|
|
275
|
+
response_status: payload[:status],
|
|
276
|
+
response_body: payload[:body]&.to_json&.truncate(500),
|
|
277
|
+
response_body_size: payload[:body]&.to_json&.size || 0)
|
|
271
278
|
otel_headers = BugBunny::OTel.messaging_headers(
|
|
272
279
|
operation: 'publish',
|
|
273
280
|
destination: '',
|
|
@@ -287,14 +294,20 @@ module BugBunny
|
|
|
287
294
|
# Maneja errores fatales asegurando que el cliente reciba una respuesta.
|
|
288
295
|
# Evita que el cliente RPC se quede esperando hasta el timeout.
|
|
289
296
|
#
|
|
297
|
+
# @param properties [Bunny::MessageProperties] Headers y propiedades AMQP.
|
|
298
|
+
# @param status [Integer] Código de estado HTTP.
|
|
299
|
+
# @param error_title [String] Título del error.
|
|
300
|
+
# @param detail [String] Detalle del error.
|
|
301
|
+
# @param exception [StandardError, nil] Excepción original (para status 500).
|
|
290
302
|
# @api private
|
|
291
|
-
def handle_fatal_error(properties, status, error_title, detail)
|
|
303
|
+
def handle_fatal_error(properties, status, error_title, detail, exception = nil)
|
|
292
304
|
return unless properties.reply_to
|
|
293
305
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
306
|
+
body = { error: error_title, detail: detail }
|
|
307
|
+
|
|
308
|
+
body[:bug_bunny_exception] = BugBunny::RemoteError.serialize(exception) if status == 500 && exception
|
|
309
|
+
|
|
310
|
+
error_payload = { status: status, body: body }
|
|
298
311
|
reply(error_payload, properties.reply_to, properties.correlation_id)
|
|
299
312
|
end
|
|
300
313
|
|
data/lib/bug_bunny/controller.rb
CHANGED
|
@@ -224,7 +224,12 @@ module BugBunny
|
|
|
224
224
|
{
|
|
225
225
|
status: 500,
|
|
226
226
|
headers: response_headers,
|
|
227
|
-
body: {
|
|
227
|
+
body: {
|
|
228
|
+
error: 'Internal Server Error',
|
|
229
|
+
detail: exception.message,
|
|
230
|
+
type: exception.class.name,
|
|
231
|
+
bug_bunny_exception: BugBunny::RemoteError.serialize(exception)
|
|
232
|
+
}
|
|
228
233
|
}
|
|
229
234
|
end
|
|
230
235
|
|
|
@@ -50,6 +50,14 @@ module BugBunny
|
|
|
50
50
|
# Pasamos el body crudo; UnprocessableEntity lo procesará en exception.rb
|
|
51
51
|
raise BugBunny::UnprocessableEntity, body
|
|
52
52
|
when 500..599
|
|
53
|
+
if body.is_a?(Hash) && body['bug_bunny_exception']
|
|
54
|
+
exception_data = body['bug_bunny_exception']
|
|
55
|
+
raise BugBunny::RemoteError.new(
|
|
56
|
+
exception_data['class'],
|
|
57
|
+
exception_data['message'],
|
|
58
|
+
exception_data['backtrace'] || []
|
|
59
|
+
)
|
|
60
|
+
end
|
|
53
61
|
raise BugBunny::InternalServerError, format_error_message(body)
|
|
54
62
|
else
|
|
55
63
|
handle_unknown_error(status, body)
|
data/lib/bug_bunny/producer.rb
CHANGED
|
@@ -89,7 +89,9 @@ module BugBunny
|
|
|
89
89
|
BugBunny.configuration.on_rpc_reply&.call(result[:headers])
|
|
90
90
|
|
|
91
91
|
safe_log(:debug, 'producer.rpc_response_received',
|
|
92
|
-
messaging_system: 'rabbitmq', messaging_operation: 'receive', messaging_message_id: cid
|
|
92
|
+
messaging_system: 'rabbitmq', messaging_operation: 'receive', messaging_message_id: cid,
|
|
93
|
+
response_body: result[:body]&.truncate(500),
|
|
94
|
+
response_headers: result[:headers]&.to_json&.truncate(300))
|
|
93
95
|
|
|
94
96
|
parse_response(result[:body])
|
|
95
97
|
ensure
|
|
@@ -125,7 +127,12 @@ module BugBunny
|
|
|
125
127
|
safe_log(:info, 'producer.publish', method: verb, path: target, **otel_fields)
|
|
126
128
|
safe_log(:debug, 'producer.publish_detail', messaging_destination_name: request.exchange,
|
|
127
129
|
exchange_opts: final_x_opts)
|
|
128
|
-
|
|
130
|
+
return unless payload.is_a?(String)
|
|
131
|
+
|
|
132
|
+
safe_log(:info, 'producer.publish_payload',
|
|
133
|
+
payload: payload.truncate(500),
|
|
134
|
+
payload_class: payload.class.name,
|
|
135
|
+
body_size: request.body.nil? ? 0 : request.body.size)
|
|
129
136
|
end
|
|
130
137
|
|
|
131
138
|
# Serializa el mensaje para su transporte.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BugBunny
|
|
4
|
+
# Error 500 especial que propagationa información de una excepción remota.
|
|
5
|
+
#
|
|
6
|
+
# Cuando un controller levanta una excepción no manejada en el worker, esta clase
|
|
7
|
+
# permite al llamador RPC acceder a:
|
|
8
|
+
# - La clase original de la excepción (ej: ActiveRecord::RecordNotFound)
|
|
9
|
+
# - El mensaje original
|
|
10
|
+
# - El backtrace completo para debugging
|
|
11
|
+
#
|
|
12
|
+
# Mantiene compatibilidad hacia atrás: si la respuesta no contiene
|
|
13
|
+
# bug_bunny_exception, se comporta como un InternalServerError común.
|
|
14
|
+
class RemoteError < ServerError
|
|
15
|
+
# @return [String] La clase de la excepción remota (ej: 'ActiveRecord::RecordNotFound').
|
|
16
|
+
attr_reader :original_class
|
|
17
|
+
|
|
18
|
+
# @return [String] El mensaje original de la excepción.
|
|
19
|
+
attr_reader :original_message
|
|
20
|
+
|
|
21
|
+
# @return [Array<String>] El backtrace original de la excepción.
|
|
22
|
+
attr_reader :original_backtrace
|
|
23
|
+
|
|
24
|
+
# Serializa una excepción para transmitirse como parte de la respuesta.
|
|
25
|
+
#
|
|
26
|
+
# @param exception [StandardError] La excepción a serializar.
|
|
27
|
+
# @param max_lines [Integer] Máximo de líneas del backtrace (default 25).
|
|
28
|
+
# @return [Hash] Estructura con class, message y backtrace.
|
|
29
|
+
def self.serialize(exception, max_lines: 25)
|
|
30
|
+
{
|
|
31
|
+
class: exception.class.name,
|
|
32
|
+
message: exception.message,
|
|
33
|
+
backtrace: exception.backtrace&.first(max_lines) || []
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Inicializa la excepción remota propagada desde el worker.
|
|
38
|
+
#
|
|
39
|
+
# @param original_class [String] Nombre completo de la clase de la excepción.
|
|
40
|
+
# @param message [String] Mensaje de la excepción.
|
|
41
|
+
# @param backtrace [Array<String>] Stack trace completo.
|
|
42
|
+
def initialize(original_class, message, backtrace)
|
|
43
|
+
@original_class = original_class
|
|
44
|
+
@original_message = message
|
|
45
|
+
@original_backtrace = backtrace || []
|
|
46
|
+
super(message)
|
|
47
|
+
set_backtrace(backtrace || [])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [String] Representación legible de la excepción.
|
|
51
|
+
def to_s
|
|
52
|
+
"#{self.class.name}(#{original_class}): #{message}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/bug_bunny/resource.rb
CHANGED
|
@@ -401,6 +401,17 @@ module BugBunny
|
|
|
401
401
|
attributes['id'] || @extra_attributes['id'] || @extra_attributes['ID'] || @extra_attributes['Id'] || @extra_attributes['_id']
|
|
402
402
|
end
|
|
403
403
|
|
|
404
|
+
# Representación legible del recurso.
|
|
405
|
+
# Muestra solo ID y atributos principales, sin detalles de infraestructura.
|
|
406
|
+
#
|
|
407
|
+
# @return [String]
|
|
408
|
+
def inspect
|
|
409
|
+
infra_keys = %w[routing_key exchange exchange_type exchange_options queue_options _id]
|
|
410
|
+
attrs = @extra_attributes.merge(attributes).reject { |k, _| infra_keys.include?(k) || k == 'id' }
|
|
411
|
+
attr_str = attrs.first(5).map { |k, v| "#{k}=#{v.inspect}" }.join(' ')
|
|
412
|
+
"#<#{self.class.name} id=#{id.inspect} persisted=#{@persisted}#{" #{attr_str}" unless attr_str.empty?}>"
|
|
413
|
+
end
|
|
414
|
+
|
|
404
415
|
def id=(value)
|
|
405
416
|
if self.class.attribute_names.include?('id')
|
|
406
417
|
super
|
|
@@ -461,6 +472,7 @@ module BugBunny
|
|
|
461
472
|
# @return [Boolean]
|
|
462
473
|
def destroy
|
|
463
474
|
return false unless persisted?
|
|
475
|
+
return false unless id
|
|
464
476
|
|
|
465
477
|
run_callbacks(:destroy) do
|
|
466
478
|
path = "#{self.class.resource_name}/#{id}"
|
data/lib/bug_bunny/version.rb
CHANGED
data/lib/bug_bunny.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'bunny'
|
|
|
4
4
|
require 'logger'
|
|
5
5
|
require_relative 'bug_bunny/version'
|
|
6
6
|
require_relative 'bug_bunny/exception'
|
|
7
|
+
require_relative 'bug_bunny/remote_error'
|
|
7
8
|
require_relative 'bug_bunny/configuration'
|
|
8
9
|
require_relative 'bug_bunny/observability'
|
|
9
10
|
require_relative 'bug_bunny/otel'
|
data/skill/SKILL.md
CHANGED
|
@@ -230,6 +230,10 @@ No registrar consumer middlewares durante la ejecución de `call()`. El stack to
|
|
|
230
230
|
**Causa:** El mensaje intenta ejecutar un controlador que no hereda de `BugBunny::Controller`.
|
|
231
231
|
**Resolución:** Verificar la jerarquía de controladores y que `config.controller_namespace` coincida.
|
|
232
232
|
|
|
233
|
+
### BugBunny::RouteNotFoundError (404)
|
|
234
|
+
**Causa:** El path del mensaje no coincide con ninguna ruta registrada. El path debe estar normalizado (sin slashes iniciales/trailing).
|
|
235
|
+
**Resolución:** Verificar que el cliente envíe el path sin leading/trailing slashes (ej: `users/42`, no `/users/42/`).
|
|
236
|
+
|
|
233
237
|
### BugBunny::UnprocessableEntity (422)
|
|
234
238
|
**Causa:** Fallo de validación en el servicio remoto.
|
|
235
239
|
**Resolución:** `resource.save` devuelve `false`. Acceder a `resource.errors` o `rescue` con `e.error_messages`.
|
|
@@ -21,13 +21,14 @@ consumer = BugBunny::Consumer.subscribe(
|
|
|
21
21
|
2. Extrae campos **OTel messaging** del mensaje para logs estructurados (sin mutar headers).
|
|
22
22
|
3. Valida que el mensaje tenga header `type` (path).
|
|
23
23
|
4. Parsea el método HTTP de headers (`x-http-method` o `method`).
|
|
24
|
-
5.
|
|
25
|
-
6.
|
|
26
|
-
7.
|
|
27
|
-
8.
|
|
28
|
-
9.
|
|
29
|
-
10.
|
|
30
|
-
11.
|
|
24
|
+
5. **Normaliza el path**: remueve slashes iniciales/trailing (`path.gsub(%r{^/|/$}, '')`).
|
|
25
|
+
6. Emite log `consumer.message_received` con campos OTel (`messaging_operation: 'process'`).
|
|
26
|
+
7. Reconoce la ruta con `BugBunny.routes.recognize(method, normalized_path)`.
|
|
27
|
+
8. Resuelve el controlador validando herencia de `BugBunny::Controller`.
|
|
28
|
+
9. Ejecuta consumer middlewares → controller callbacks → acción.
|
|
29
|
+
10. Responde via `reply_to` si está presente (RPC), inyectando campos OTel (`messaging_operation: 'publish'`).
|
|
30
|
+
11. Emite log `consumer.message_processed` con campos OTel y duraciones.
|
|
31
|
+
12. Hace `ack` del mensaje. En caso de error, `reject`.
|
|
31
32
|
|
|
32
33
|
## Observability: OTel Fields
|
|
33
34
|
|
data/skills.lock
CHANGED
|
@@ -110,4 +110,48 @@ RSpec.describe BugBunny::Resource, :integration do
|
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
|
+
|
|
114
|
+
describe '#inspect' do
|
|
115
|
+
let(:node) { SpecNode.new(id: '123', name: 'test-node', status: 'active', ip: '192.168.1.1', port: 8080) }
|
|
116
|
+
|
|
117
|
+
it 'muestra id y persisted' do
|
|
118
|
+
expect(node.inspect).to include('id="123"')
|
|
119
|
+
expect(node.inspect).to include('persisted=false')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'muestra atributos principales sin detalles de infraestructura' do
|
|
123
|
+
result = node.inspect
|
|
124
|
+
|
|
125
|
+
expect(result).to include('name="test-node"')
|
|
126
|
+
expect(result).to include('status="active"')
|
|
127
|
+
expect(result).to include('ip="192.168.1.1"')
|
|
128
|
+
expect(result).to include('port=8080')
|
|
129
|
+
expect(result).not_to include('routing_key')
|
|
130
|
+
expect(result).not_to include('exchange')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'filtra atributos de infraestructura cuando están presentes' do
|
|
134
|
+
node.routing_key = 'radius_1'
|
|
135
|
+
node.exchange = 'test_exchange'
|
|
136
|
+
node.exchange_type = 'topic'
|
|
137
|
+
node.exchange_options = { durable: true }
|
|
138
|
+
node.persisted = true
|
|
139
|
+
|
|
140
|
+
result = node.inspect
|
|
141
|
+
|
|
142
|
+
expect(result).not_to include('routing_key')
|
|
143
|
+
expect(result).not_to include('exchange')
|
|
144
|
+
expect(result).not_to include('exchange_type')
|
|
145
|
+
expect(result).not_to include('exchange_options')
|
|
146
|
+
expect(result).to include('persisted=true')
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'limita a 5 atributos principales' do
|
|
150
|
+
node = SpecNode.new(id: '1', a: '1', b: '2', c: '3', d: '4', e: '5', f: '6')
|
|
151
|
+
|
|
152
|
+
result = node.inspect
|
|
153
|
+
|
|
154
|
+
expect(result.scan('=').length).to be <= 7 # id + persisted + max 5 attrs
|
|
155
|
+
end
|
|
156
|
+
end
|
|
113
157
|
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe BugBunny::Routing::Route do
|
|
6
|
+
describe '#match? and #extract_params — path normalization' do
|
|
7
|
+
let(:route) { described_class.new('GET', 'services/otaigccd59q0k7kxb1h193go2/restart', to: 'services#restart') }
|
|
8
|
+
|
|
9
|
+
it 'matches path without leading slash' do
|
|
10
|
+
expect(route.match?('GET', 'services/otaigccd59q0k7kxb1h193go2/restart')).to be true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'matches path with leading slash' do
|
|
14
|
+
expect(route.match?('GET', '/services/otaigccd59q0k7kxb1h193go2/restart')).to be true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'matches path with trailing slash' do
|
|
18
|
+
expect(route.match?('GET', 'services/otaigccd59q0k7kxb1h193go2/restart/')).to be true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'matches path with leading and trailing slash' do
|
|
22
|
+
expect(route.match?('GET', '/services/otaigccd59q0k7kxb1h193go2/restart/')).to be true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'extracts params from path without slash' do
|
|
26
|
+
params = route.extract_params('services/otaigccd59q0k7kxb1h193go2/restart')
|
|
27
|
+
expect(params).to eq({})
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'extracts params from path with slash' do
|
|
31
|
+
params = route.extract_params('/services/otaigccd59q0k7kxb1h193go2/restart')
|
|
32
|
+
expect(params).to eq({})
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context 'route with dynamic segment' do
|
|
36
|
+
let(:route_with_id) { described_class.new('GET', 'nodes/:id', to: 'nodes#show') }
|
|
37
|
+
|
|
38
|
+
it 'extracts params from path without slash' do
|
|
39
|
+
params = route_with_id.extract_params('nodes/123')
|
|
40
|
+
expect(params).to eq({ 'id' => '123' })
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'extracts params from path with slash' do
|
|
44
|
+
params = route_with_id.extract_params('/nodes/123')
|
|
45
|
+
expect(params).to eq({ 'id' => '123' })
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context 'route defined with leading slash' do
|
|
50
|
+
let(:route_with_leading_slash) do
|
|
51
|
+
described_class.new('GET', '/services/otaigccd59q0k7kxb1h193go2/restart', to: 'services#restart')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'matches path without slash' do
|
|
55
|
+
expect(route_with_leading_slash.match?('GET', 'services/otaigccd59q0k7kxb1h193go2/restart')).to be true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'matches path with slash' do
|
|
59
|
+
expect(route_with_leading_slash.match?('GET', '/services/otaigccd59q0k7kxb1h193go2/restart')).to be true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe 'RouteSet#recognize path normalization' do
|
|
65
|
+
let(:route_set) { BugBunny::Routing::RouteSet.new }
|
|
66
|
+
|
|
67
|
+
before do
|
|
68
|
+
route_set.draw do
|
|
69
|
+
get 'services/otaigccd59q0k7kxb1h193go2/restart', to: 'services#restart'
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'recognizes path without slash' do
|
|
74
|
+
result = route_set.recognize('GET', 'services/otaigccd59q0k7kxb1h193go2/restart')
|
|
75
|
+
expect(result).not_to be_nil
|
|
76
|
+
expect(result[:controller]).to eq('services')
|
|
77
|
+
expect(result[:action]).to eq('restart')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'recognizes path with leading slash' do
|
|
81
|
+
result = route_set.recognize('GET', '/services/otaigccd59q0k7kxb1h193go2/restart')
|
|
82
|
+
expect(result).not_to be_nil
|
|
83
|
+
expect(result[:controller]).to eq('services')
|
|
84
|
+
expect(result[:action]).to eq('restart')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'returns nil for non-matching path' do
|
|
88
|
+
result = route_set.recognize('GET', 'services/nonexistent')
|
|
89
|
+
expect(result).to be_nil
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe 'Consumer path handling — URI parsing edge case' do
|
|
94
|
+
let(:channel) { BunnyMocks::FakeChannel.new(true) }
|
|
95
|
+
let(:connection) { BunnyMocks::FakeConnection.new(true, channel) }
|
|
96
|
+
|
|
97
|
+
let(:mock_channel) do
|
|
98
|
+
ch = double('channel')
|
|
99
|
+
allow(ch).to receive(:reject)
|
|
100
|
+
allow(ch).to receive(:ack)
|
|
101
|
+
allow(ch).to receive(:open?).and_return(true)
|
|
102
|
+
default_ex = double('default_exchange')
|
|
103
|
+
allow(default_ex).to receive(:publish)
|
|
104
|
+
allow(ch).to receive(:default_exchange).and_return(default_ex)
|
|
105
|
+
ch
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
let(:mock_session) do
|
|
109
|
+
s = instance_double(BugBunny::Session)
|
|
110
|
+
allow(s).to receive(:exchange).and_return(double('exchange'))
|
|
111
|
+
allow(s).to receive(:queue).and_return(double('queue'))
|
|
112
|
+
allow(s).to receive(:close)
|
|
113
|
+
allow(s).to receive(:channel).and_return(mock_channel)
|
|
114
|
+
s
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
let(:test_consumer) do
|
|
118
|
+
c = BugBunny::Consumer.new(connection)
|
|
119
|
+
c.instance_variable_set(:@session, mock_session)
|
|
120
|
+
c
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
let(:delivery_info) do
|
|
124
|
+
double('delivery_info',
|
|
125
|
+
exchange: 'events_x',
|
|
126
|
+
routing_key: 'users.created',
|
|
127
|
+
delivery_tag: 'delivery-tag-1',
|
|
128
|
+
redelivered?: false)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
let(:properties) do
|
|
132
|
+
double('properties',
|
|
133
|
+
type: 'services/otaigccd59q0k7kxb1h193go2/restart',
|
|
134
|
+
headers: { 'x-http-method' => 'GET' },
|
|
135
|
+
correlation_id: 'corr-abc-123',
|
|
136
|
+
reply_to: nil,
|
|
137
|
+
content_type: 'application/json')
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
let(:logged_events) { [] }
|
|
141
|
+
|
|
142
|
+
before do
|
|
143
|
+
allow(test_consumer).to receive(:safe_log) do |level, event, **kwargs|
|
|
144
|
+
logged_events << { level: level, event: event, kwargs: kwargs }
|
|
145
|
+
end
|
|
146
|
+
allow(test_consumer).to receive(:handle_fatal_error)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'logs route_not_found with normalized path (without extra slash)' do
|
|
150
|
+
allow(BugBunny.routes).to receive(:recognize).and_return(nil)
|
|
151
|
+
|
|
152
|
+
test_consumer.send(:process_message, delivery_info, properties, '{}')
|
|
153
|
+
|
|
154
|
+
route_not_found_event = logged_events.find { |e| e[:event] == 'consumer.route_not_found' }
|
|
155
|
+
expect(route_not_found_event).not_to be_nil
|
|
156
|
+
expect(route_not_found_event[:kwargs][:path]).to eq('services/otaigccd59q0k7kxb1h193go2/restart')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
context 'when properties.type has leading slash' do
|
|
160
|
+
let(:properties) do
|
|
161
|
+
double('properties',
|
|
162
|
+
type: '/services/otaigccd59q0k7kxb1h193go2/restart',
|
|
163
|
+
headers: { 'x-http-method' => 'GET' },
|
|
164
|
+
correlation_id: 'corr-abc-123',
|
|
165
|
+
reply_to: nil,
|
|
166
|
+
content_type: 'application/json')
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'logs route_not_found with normalized path (stripped leading slash)' do
|
|
170
|
+
allow(BugBunny.routes).to receive(:recognize).and_return(nil)
|
|
171
|
+
|
|
172
|
+
test_consumer.send(:process_message, delivery_info, properties, '{}')
|
|
173
|
+
|
|
174
|
+
route_not_found_event = logged_events.find { |e| e[:event] == 'consumer.route_not_found' }
|
|
175
|
+
expect(route_not_found_event).not_to be_nil
|
|
176
|
+
expect(route_not_found_event[:kwargs][:path]).to eq('services/otaigccd59q0k7kxb1h193go2/restart')
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
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: 4.
|
|
4
|
+
version: 4.10.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-04-
|
|
11
|
+
date: 2026-04-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -249,6 +249,7 @@ files:
|
|
|
249
249
|
- lib/bug_bunny/otel.rb
|
|
250
250
|
- lib/bug_bunny/producer.rb
|
|
251
251
|
- lib/bug_bunny/railtie.rb
|
|
252
|
+
- lib/bug_bunny/remote_error.rb
|
|
252
253
|
- lib/bug_bunny/request.rb
|
|
253
254
|
- lib/bug_bunny/resource.rb
|
|
254
255
|
- lib/bug_bunny/routing/route.rb
|
|
@@ -289,6 +290,7 @@ files:
|
|
|
289
290
|
- spec/unit/producer_spec.rb
|
|
290
291
|
- spec/unit/request_spec.rb
|
|
291
292
|
- spec/unit/resource_attributes_spec.rb
|
|
293
|
+
- spec/unit/route_spec.rb
|
|
292
294
|
- spec/unit/session_spec.rb
|
|
293
295
|
- test/integration/infrastructure_test.rb
|
|
294
296
|
- test/integration/manual_client_test.rb
|
|
@@ -300,7 +302,7 @@ metadata:
|
|
|
300
302
|
homepage_uri: https://github.com/gedera/bug_bunny
|
|
301
303
|
source_code_uri: https://github.com/gedera/bug_bunny
|
|
302
304
|
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
|
303
|
-
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.
|
|
305
|
+
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.10.1/skill
|
|
304
306
|
post_install_message:
|
|
305
307
|
rdoc_options: []
|
|
306
308
|
require_paths:
|