bug_bunny 4.10.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af9746f85e544f059873513a7ace9b478acb6664b9053bb90d1963afb1d72848
4
- data.tar.gz: 32854fb9e2a67f8e5cb537550d957704443d95183d22d5305b48c3207edaf05b
3
+ metadata.gz: 1e1a5c8c069bdf7634c57b1df16aa226d5abc8fd6e864596d273431a680a1b55
4
+ data.tar.gz: 49b7cf39e53fe8c3f3b90331cfc4f171c8769264356ba1c1d4a767bf27b868ee
5
5
  SHA512:
6
- metadata.gz: 90169fb942803e87f47cf1af2fb2b4cb4d66112aaf4ab6632bb6a8d5031383e1fc0105a4cf2449eed847822509a71c1e4695c8cacb9907889c286cc5cfefe70b
7
- data.tar.gz: 39262e92b89fb583769bcfaa8a17972584b0aaeda9f197ff3cf7cf4c45941ca0ee0b3aa154e8073d4004f5213bb1852aa16cc6c6f809951b2bcf3c76b85fa66d
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
@@ -181,18 +181,20 @@ module BugBunny
181
181
  # ===================================================================
182
182
  # 3. Ruteo Declarativo
183
183
  # ===================================================================
184
- uri = URI.parse("http://dummy/#{path}")
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, uri.path)
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: uri.path)
195
- handle_fatal_error(properties, 404, 'Not Found', "No route matches [#{http_method}] \"/#{uri.path}\"")
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = '4.10.0'
4
+ VERSION = '4.10.1'
5
5
  end
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. Emite log `consumer.message_received` con campos OTel (`messaging_operation: 'process'`).
25
- 6. Reconoce la ruta con `BugBunny.routes.recognize(method, path)`.
26
- 7. Resuelve el controlador validando herencia de `BugBunny::Controller`.
27
- 8. Ejecuta consumer middlewares controller callbacks → acción.
28
- 9. Responde via `reply_to` si está presente (RPC), inyectando campos OTel (`messaging_operation: 'publish'`).
29
- 10. Emite log `consumer.message_processed` con campos OTel y duraciones.
30
- 11. Hace `ack` del mensaje. En caso de error, `reject`.
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
 
@@ -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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bug_bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.10.0
4
+ version: 4.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix
@@ -290,6 +290,7 @@ files:
290
290
  - spec/unit/producer_spec.rb
291
291
  - spec/unit/request_spec.rb
292
292
  - spec/unit/resource_attributes_spec.rb
293
+ - spec/unit/route_spec.rb
293
294
  - spec/unit/session_spec.rb
294
295
  - test/integration/infrastructure_test.rb
295
296
  - test/integration/manual_client_test.rb
@@ -301,7 +302,7 @@ metadata:
301
302
  homepage_uri: https://github.com/gedera/bug_bunny
302
303
  source_code_uri: https://github.com/gedera/bug_bunny
303
304
  changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
304
- documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.10.0/skill
305
+ documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.10.1/skill
305
306
  post_install_message:
306
307
  rdoc_options: []
307
308
  require_paths: