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.
@@ -9,15 +9,13 @@ require 'rack/utils'
9
9
  module BugBunny
10
10
  # Clase base para modelos remotos que implementan **Active Record over AMQP (RESTful)**.
11
11
  #
12
- # Soporta un esquema híbrido de datos:
13
- # 1. **Atributos Tipados:** Definidos con `attribute :name, :type`.
14
- # 2. **Atributos Dinámicos:** Asignados al vuelo sin definición previa.
15
- #
16
- # Implementa un sistema de "Dirty Tracking" híbrido para detectar cambios
17
- # tanto en atributos tipados (via ActiveModel) como dinámicos (via Set manual).
12
+ # Soporta un esquema híbrido de datos y configuración de infraestructura en cascada:
13
+ # 1. **Defaults:** Definidos en la sesión.
14
+ # 2. **Global:** Definidos en BugBunny.configuration.
15
+ # 3. **Específico:** Definidos en la clase del recurso o vía `with`.
18
16
  #
19
17
  # @author Gabriel
20
- # @since 3.0.6
18
+ # @since 3.1.0
21
19
  class Resource
22
20
  include ActiveModel::API
23
21
  include ActiveModel::Attributes
@@ -31,11 +29,22 @@ module BugBunny
31
29
  attr_accessor :persisted
32
30
  attr_accessor :routing_key, :exchange, :exchange_type
33
31
 
32
+ # @return [Hash] Opciones específicas de instancia para exchange y queue.
33
+ attr_accessor :exchange_options, :queue_options
34
+
34
35
  class << self
35
36
  attr_writer :connection_pool, :exchange, :exchange_type, :resource_name, :routing_key, :param_key
36
37
 
38
+ # @!group Configuración de Infraestructura Específica
39
+ attr_writer :exchange_options, :queue_options
40
+
41
+ # @api private
37
42
  def thread_config(key); Thread.current["bb_#{object_id}_#{key}"]; end
38
43
 
44
+ # Resuelve la configuración buscando en el hilo, luego en la jerarquía de clases.
45
+ # @param key [Symbol] Clave en el Thread.current.
46
+ # @param instance_var [Symbol] Nombre de la variable de instancia en la clase.
47
+ # @return [Object, nil]
39
48
  def resolve_config(key, instance_var)
40
49
  val = thread_config(key)
41
50
  return val if val
@@ -48,23 +57,38 @@ module BugBunny
48
57
  nil
49
58
  end
50
59
 
60
+ # @return [ConnectionPool, nil]
51
61
  def connection_pool; resolve_config(:pool, :@connection_pool); end
52
- def current_exchange; resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined"); end
62
+
63
+ # @return [String] Nombre del exchange actual.
64
+ def current_exchange; resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined for #{name}"); end
65
+
66
+ # @return [String] Tipo de exchange ('direct', 'topic', 'fanout').
53
67
  def current_exchange_type; resolve_config(:exchange_type, :@exchange_type) || 'direct'; end
54
68
 
69
+ # @return [Hash] Opciones de exchange específicas (Nivel 3 de la cascada).
70
+ def current_exchange_options; resolve_config(:exchange_options, :@exchange_options) || {}; end
71
+
72
+ # @return [Hash] Opciones de cola específicas.
73
+ def current_queue_options; resolve_config(:queue_options, :@queue_options) || {}; end
74
+
75
+ # @return [String] Nombre del recurso para la construcción de rutas.
55
76
  def resource_name
56
77
  resolve_config(:resource_name, :@resource_name) || name.demodulize.underscore.pluralize
57
78
  end
58
79
 
80
+ # @return [String] Clave raíz para envolver el payload en las peticiones.
59
81
  def param_key
60
82
  resolve_config(:param_key, :@param_key) || model_name.element
61
83
  end
62
84
 
85
+ # @api private
63
86
  def client_middleware(&block)
64
87
  @client_middleware_stack ||= []
65
88
  @client_middleware_stack << block
66
89
  end
67
90
 
91
+ # @api private
68
92
  def resolve_middleware_stack
69
93
  stack = []
70
94
  target = self
@@ -76,6 +100,8 @@ module BugBunny
76
100
  stack
77
101
  end
78
102
 
103
+ # Instancia el cliente inyectando los middlewares configurados.
104
+ # @return [BugBunny::Client]
79
105
  def bug_bunny_client
80
106
  pool = connection_pool
81
107
  raise BugBunny::Error, "Connection pool missing for #{name}" unless pool
@@ -84,8 +110,23 @@ module BugBunny
84
110
  end
85
111
  end
86
112
 
87
- def with(exchange: nil, routing_key: nil, exchange_type: nil, pool: nil)
88
- keys = { exchange: "bb_#{object_id}_exchange", exchange_type: "bb_#{object_id}_exchange_type", pool: "bb_#{object_id}_pool", routing_key: "bb_#{object_id}_routing_key" }
113
+ # Permite configurar dinámicamente el contexto AMQP para una operación.
114
+ #
115
+ # @param exchange [String] Nombre del exchange.
116
+ # @param routing_key [String] Routing key manual.
117
+ # @param exchange_type [String] Tipo de exchange.
118
+ # @param pool [ConnectionPool] Pool de conexiones.
119
+ # @param exchange_options [Hash] Opciones de infraestructura.
120
+ # @param queue_options [Hash] Opciones de cola.
121
+ def with(exchange: nil, routing_key: nil, exchange_type: nil, pool: nil, exchange_options: nil, queue_options: nil)
122
+ keys = {
123
+ exchange: "bb_#{object_id}_exchange",
124
+ exchange_type: "bb_#{object_id}_exchange_type",
125
+ pool: "bb_#{object_id}_pool",
126
+ routing_key: "bb_#{object_id}_routing_key",
127
+ exchange_options: "bb_#{object_id}_exchange_options",
128
+ queue_options: "bb_#{object_id}_queue_options"
129
+ }
89
130
  old_values = {}
90
131
  keys.each { |k, v| old_values[k] = Thread.current[v] }
91
132
 
@@ -93,6 +134,8 @@ module BugBunny
93
134
  Thread.current[keys[:exchange_type]] = exchange_type if exchange_type
94
135
  Thread.current[keys[:pool]] = pool if pool
95
136
  Thread.current[keys[:routing_key]] = routing_key if routing_key
137
+ Thread.current[keys[:exchange_options]] = exchange_options if exchange_options
138
+ Thread.current[keys[:queue_options]] = queue_options if queue_options
96
139
 
97
140
  if block_given?
98
141
  begin; yield; ensure; keys.each { |k, v| Thread.current[v] = old_values[k] }; end
@@ -101,11 +144,15 @@ module BugBunny
101
144
  end
102
145
  end
103
146
 
147
+ # Proxy para el encadenamiento del método `.with`.
104
148
  class ScopeProxy < BasicObject
105
149
  def initialize(target, keys, old_values); @target = target; @keys = keys; @old_values = old_values; end
106
150
  def method_missing(method, *args, &block); @target.public_send(method, *args, &block); ensure; @keys.each { |k, v| ::Thread.current[v] = @old_values[k] }; end
107
151
  end
108
152
 
153
+ # Calcula la routing key final.
154
+ # @param id [String, nil] ID del recurso.
155
+ # @return [String]
109
156
  def calculate_routing_key(id = nil)
110
157
  manual_rk = thread_config(:routing_key)
111
158
  return manual_rk if manual_rk
@@ -115,11 +162,25 @@ module BugBunny
115
162
  end
116
163
 
117
164
  # @!group Acciones CRUD RESTful
165
+
166
+ # Realiza una búsqueda filtrada (GET).
167
+ # @param filters [Hash]
168
+ # @return [Array<BugBunny::Resource>]
118
169
  def where(filters = {})
119
170
  rk = calculate_routing_key
120
171
  path = resource_name
121
172
  path += "?#{Rack::Utils.build_nested_query(filters)}" if filters.present?
122
- response = bug_bunny_client.request(path, method: :get, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk)
173
+
174
+ response = bug_bunny_client.request(
175
+ path,
176
+ method: :get,
177
+ exchange: current_exchange,
178
+ exchange_type: current_exchange_type,
179
+ routing_key: rk,
180
+ exchange_options: current_exchange_options,
181
+ queue_options: current_queue_options
182
+ )
183
+
123
184
  return [] unless response['body'].is_a?(Array)
124
185
  response['body'].map do |attrs|
125
186
  inst = new(attrs)
@@ -129,12 +190,27 @@ module BugBunny
129
190
  end
130
191
  end
131
192
 
193
+ # Devuelve todos los registros.
194
+ # @return [Array<BugBunny::Resource>]
132
195
  def all; where({}); end
133
196
 
197
+ # Busca un registro por ID (GET).
198
+ # @param id [String, Integer]
199
+ # @return [BugBunny::Resource, nil]
134
200
  def find(id)
135
201
  rk = calculate_routing_key(id)
136
202
  path = "#{resource_name}/#{id}"
137
- response = bug_bunny_client.request(path, method: :get, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk)
203
+
204
+ response = bug_bunny_client.request(
205
+ path,
206
+ method: :get,
207
+ exchange: current_exchange,
208
+ exchange_type: current_exchange_type,
209
+ routing_key: rk,
210
+ exchange_options: current_exchange_options,
211
+ queue_options: current_queue_options
212
+ )
213
+
138
214
  return nil if response.nil? || response['status'] == 404
139
215
  return nil unless response['body'].is_a?(Hash)
140
216
  instance = new(response['body'])
@@ -143,6 +219,9 @@ module BugBunny
143
219
  instance
144
220
  end
145
221
 
222
+ # Crea una nueva instancia y la persiste.
223
+ # @param payload [Hash]
224
+ # @return [BugBunny::Resource]
146
225
  def create(payload)
147
226
  instance = new(payload)
148
227
  instance.save
@@ -152,13 +231,19 @@ module BugBunny
152
231
 
153
232
  # @!group Instancia
154
233
 
234
+ # Inicializa el recurso.
235
+ # @param attributes [Hash]
155
236
  def initialize(attributes = {})
156
237
  @remote_attributes = {}.with_indifferent_access
157
238
  @dynamic_changes = Set.new # Rastreo manual para atributos dinámicos
158
239
  @persisted = false
240
+
241
+ # Contexto de infraestructura
159
242
  @routing_key = self.class.thread_config(:routing_key)
160
243
  @exchange = self.class.thread_config(:exchange)
161
244
  @exchange_type = self.class.thread_config(:exchange_type)
245
+ @exchange_options = self.class.thread_config(:exchange_options) || self.class.current_exchange_options
246
+ @queue_options = self.class.thread_config(:queue_options) || self.class.current_queue_options
162
247
 
163
248
  super()
164
249
  assign_attributes(attributes)
@@ -170,30 +255,46 @@ module BugBunny
170
255
  @dynamic_changes.clear
171
256
  end
172
257
 
258
+ # Serialización combinada.
259
+ # @return [Hash]
173
260
  def attributes_for_serialization
174
261
  @remote_attributes.merge(attributes)
175
262
  end
176
263
 
264
+ # @return [String]
177
265
  def calculate_routing_key(id=nil); @routing_key || self.class.calculate_routing_key(id); end
266
+
267
+ # @return [String]
178
268
  def current_exchange; @exchange || self.class.current_exchange; end
269
+
270
+ # @return [String]
179
271
  def current_exchange_type; @exchange_type || self.class.current_exchange_type; end
272
+
273
+ # @return [BugBunny::Client]
180
274
  def bug_bunny_client; self.class.bug_bunny_client; end
275
+
276
+ # @return [Boolean]
181
277
  def persisted?; !!@persisted; end
182
278
 
279
+ # Asignación masiva de atributos.
280
+ # @param new_attributes [Hash]
183
281
  def assign_attributes(new_attributes)
184
282
  return if new_attributes.nil?
185
283
  new_attributes.each { |k, v| public_send("#{k}=", v) }
186
284
  end
187
285
 
286
+ # Actualiza y guarda.
287
+ # @param attributes [Hash]
288
+ # @return [Boolean]
188
289
  def update(attributes)
189
290
  assign_attributes(attributes)
190
291
  save
191
292
  end
192
293
 
193
294
  # Retorna el hash combinado de cambios (Tipados + Dinámicos).
295
+ # @return [Hash]
194
296
  def changes_to_send
195
297
  # 1. Cambios de ActiveModel (Tipados)
196
- # changes returns { 'attr' => [old, new] } -> nos quedamos con new
197
298
  payload = changes.transform_values(&:last)
198
299
 
199
300
  # 2. Cambios Dinámicos (Manuales)
@@ -214,8 +315,6 @@ module BugBunny
214
315
  key = attribute_name.chop
215
316
  val = args.first
216
317
 
217
- # Dirty Tracking Manual
218
- # Si el valor cambia, lo marcamos en nuestro Set
219
318
  if @remote_attributes[key] != val
220
319
  @dynamic_changes << key
221
320
  end
@@ -230,6 +329,7 @@ module BugBunny
230
329
  @remote_attributes.key?(method_name.to_s.sub(/=$/, '')) || super
231
330
  end
232
331
 
332
+ # @return [Object] Valor del ID buscando en múltiples nomenclaturas.
233
333
  def id
234
334
  attributes['id'] || @remote_attributes['id'] || @remote_attributes['ID'] || @remote_attributes['Id'] || @remote_attributes['_id']
235
335
  end
@@ -249,6 +349,8 @@ module BugBunny
249
349
 
250
350
  # @!group Persistencia
251
351
 
352
+ # Guarda el recurso en el servidor remoto vía AMQP (POST o PUT).
353
+ # @return [Boolean]
252
354
  def save
253
355
  return false unless valid?
254
356
 
@@ -268,6 +370,8 @@ module BugBunny
268
370
  exchange: current_exchange,
269
371
  exchange_type: current_exchange_type,
270
372
  routing_key: rk,
373
+ exchange_options: @exchange_options,
374
+ queue_options: @queue_options,
271
375
  body: wrapped_payload
272
376
  )
273
377
 
@@ -278,12 +382,24 @@ module BugBunny
278
382
  false
279
383
  end
280
384
 
385
+ # Elimina el recurso del servidor remoto (DELETE).
386
+ # @return [Boolean]
281
387
  def destroy
282
388
  return false unless persisted?
283
389
  run_callbacks(:destroy) do
284
390
  path = "#{self.class.resource_name}/#{id}"
285
391
  rk = calculate_routing_key(id)
286
- bug_bunny_client.request(path, method: :delete, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk)
392
+
393
+ bug_bunny_client.request(
394
+ path,
395
+ method: :delete,
396
+ exchange: current_exchange,
397
+ exchange_type: current_exchange_type,
398
+ routing_key: rk,
399
+ exchange_options: @exchange_options,
400
+ queue_options: @queue_options
401
+ )
402
+
287
403
  self.persisted = false
288
404
  end
289
405
  true
@@ -293,6 +409,7 @@ module BugBunny
293
409
 
294
410
  private
295
411
 
412
+ # Maneja la lógica de respuesta para la acción de guardado.
296
413
  def handle_save_response(response)
297
414
  if response['status'] == 422
298
415
  raise BugBunny::UnprocessableEntity.new(response['body']['errors'] || response['body'])
@@ -301,12 +418,14 @@ module BugBunny
301
418
  elsif response['status'] >= 400
302
419
  raise BugBunny::ClientError
303
420
  end
421
+
304
422
  assign_attributes(response['body'])
305
423
  self.persisted = true
306
424
  clear_changes_information
307
425
  true
308
426
  end
309
427
 
428
+ # Carga errores remotos en el objeto local.
310
429
  def load_remote_rabbit_errors(errors_hash)
311
430
  return if errors_hash.nil?
312
431
  if errors_hash.is_a?(String)
@@ -1,18 +1,23 @@
1
- # lib/bug_bunny/session.rb
1
+ # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
4
  # Clase interna que encapsula una unidad de trabajo sobre una conexión RabbitMQ.
5
5
  #
6
- # Gestiona el ciclo de vida de un `Bunny::Channel` implementando:
7
- # 1. Carga Perezosa (Lazy Loading): El canal solo se abre al usarse.
8
- # 2. Resiliencia: Intenta recuperar la conexión TCP si está cerrada.
6
+ # Implementa la lógica de "Configuración en Cascada" para Exchanges y Colas,
7
+ # gestionando el ciclo de vida de un `Bunny::Channel` con resiliencia y carga perezosa.
9
8
  #
10
9
  # @api private
11
10
  class Session
12
- # Opciones por defecto (Mantenemos las que tenías en tu repo)
11
+ # @!group Opciones por Defecto (Nivel 1: Gema)
12
+
13
+ # Opciones predeterminadas de la gema para Exchanges.
13
14
  DEFAULT_EXCHANGE_OPTIONS = { durable: false, auto_delete: false }.freeze
15
+
16
+ # Opciones predeterminadas de la gema para Colas.
14
17
  DEFAULT_QUEUE_OPTIONS = { exclusive: false, durable: false, auto_delete: true }.freeze
15
18
 
19
+ # @!endgroup
20
+
16
21
  # @return [Bunny::Session] La conexión TCP subyacente.
17
22
  attr_reader :connection
18
23
 
@@ -42,30 +47,50 @@ module BugBunny
42
47
  @channel
43
48
  end
44
49
 
45
- # Factory method para declarar o recuperar un Exchange.
46
- # Usa el método robusto `channel` internamente.
50
+ # Factory method para declarar o recuperar un Exchange aplicando la cascada de configuración.
51
+ #
52
+ # Jerarquía de fusión:
53
+ # 1. Defaults de la gema (`DEFAULT_EXCHANGE_OPTIONS`)
54
+ # 2. Configuración global (`BugBunny.configuration.exchange_options`)
55
+ # 3. Opciones específicas pasadas como argumento (`opts`)
47
56
  #
48
57
  # @param name [String, nil] Nombre del exchange.
49
- # @param type [String, Symbol] Tipo de exchange.
50
- # @param opts [Hash] Opciones adicionales.
58
+ # @param type [String, Symbol] Tipo de exchange ('direct', 'topic', 'fanout').
59
+ # @param opts [Hash] Opciones específicas de infraestructura para este intercambio.
60
+ # @return [Bunny::Exchange] El objeto exchange de Bunny configurado.
51
61
  def exchange(name: nil, type: 'direct', opts: {})
52
62
  return channel.default_exchange if name.nil? || name.empty?
53
63
 
54
- merged_opts = DEFAULT_EXCHANGE_OPTIONS.merge(opts)
55
- # public_send permite llamar a :topic, :direct, etc. dinámicamente
64
+ # Aplicación de la lógica de fusión en cascada
65
+ merged_opts = DEFAULT_EXCHANGE_OPTIONS
66
+ .merge(BugBunny.configuration.exchange_options || {})
67
+ .merge(opts)
68
+
69
+ # public_send permite llamar a :topic, :direct, etc. dinámicamente según el tipo
56
70
  channel.public_send(type, name, merged_opts)
57
71
  end
58
72
 
59
- # Factory method para declarar o recuperar una Cola.
60
- # Usa el método robusto `channel` internamente.
73
+ # Factory method para declarar o recuperar una Cola aplicando la cascada de configuración.
74
+ #
75
+ # Jerarquía de fusión:
76
+ # 1. Defaults de la gema (`DEFAULT_QUEUE_OPTIONS`)
77
+ # 2. Configuración global (`BugBunny.configuration.queue_options`)
78
+ # 3. Opciones específicas pasadas como argumento (`opts`)
61
79
  #
62
80
  # @param name [String] Nombre de la cola.
63
- # @param opts [Hash] Opciones adicionales.
81
+ # @param opts [Hash] Opciones específicas de infraestructura para esta cola.
82
+ # @return [Bunny::Queue] El objeto cola de Bunny configurado.
64
83
  def queue(name, opts = {})
65
- channel.queue(name.to_s, DEFAULT_QUEUE_OPTIONS.merge(opts))
84
+ # Aplicación de la lógica de fusión en cascada
85
+ merged_opts = DEFAULT_QUEUE_OPTIONS
86
+ .merge(BugBunny.configuration.queue_options || {})
87
+ .merge(opts)
88
+
89
+ channel.queue(name.to_s, merged_opts)
66
90
  end
67
91
 
68
92
  # Cierra el canal asociado a esta sesión de forma segura.
93
+ # @return [void]
69
94
  def close
70
95
  @channel&.close if @channel&.open?
71
96
  @channel = nil
@@ -73,8 +98,10 @@ module BugBunny
73
98
 
74
99
  private
75
100
 
76
- # Crea y configura un nuevo canal.
101
+ # Crea y configura un nuevo canal con las preferencias globales.
77
102
  # Asume que la conexión ya ha sido verificada por `ensure_connection!`.
103
+ #
104
+ # @raise [BugBunny::CommunicationError] Si falla la creación del canal.
78
105
  def create_channel!
79
106
  @channel = @connection.create_channel
80
107
 
@@ -90,13 +117,15 @@ module BugBunny
90
117
 
91
118
  # Garantiza que la conexión TCP esté abierta.
92
119
  # Si está cerrada, intenta reconectarla (Reconexión Transparente).
120
+ #
121
+ # @raise [BugBunny::CommunicationError] Si falla la reconexión.
93
122
  def ensure_connection!
94
123
  return if @connection.open?
95
124
 
96
- BugBunny.configuration.logger.warn("[BugBunny] Connection lost. Attempting to reconnect...")
125
+ BugBunny.configuration.logger.warn("[BugBunny::Session] ⚠️ Connection lost. Attempting to reconnect...")
97
126
  @connection.start
98
127
  rescue StandardError => e
99
- BugBunny.configuration.logger.error("[BugBunny] Critical connection failure: #{e.message}")
128
+ BugBunny.configuration.logger.error("[BugBunny::Session] Critical connection failure: #{e.message}")
100
129
  raise BugBunny::CommunicationError, "Could not reconnect to RabbitMQ: #{e.message}"
101
130
  end
102
131
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = "3.1.0"
4
+ VERSION = "3.1.1"
5
5
  end
data/lib/bug_bunny.rb CHANGED
@@ -75,7 +75,7 @@ module BugBunny
75
75
 
76
76
  @global_connection.close if @global_connection.open?
77
77
  @global_connection = nil
78
- configuration.logger.info('[BugBunny] Global connection closed.')
78
+ configuration.logger.info('[BugBunny] 🔌 Global connection closed.')
79
79
  end
80
80
 
81
81
  # @api private
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_helper'
4
+
5
+ # --- CLASES DE PRUEBA (Namespace Aislado) ---
6
+ module InfraTest
7
+ class PingController < BugBunny::Controller
8
+ # Agregamos SHOW para soportar el .find del test
9
+ def show
10
+ render status: 200, json: { id: params[:id], message: 'pong', namespace: 'InfraTest' }
11
+ end
12
+
13
+ def index
14
+ render status: 200, json: { message: 'pong_index', namespace: 'InfraTest' }
15
+ end
16
+ end
17
+ end
18
+
19
+ class InfraResource < BugBunny::Resource
20
+ self.resource_name = 'ping'
21
+ self.exchange = 'test_infra_exchange'
22
+ self.exchange_type = 'topic'
23
+ end
24
+
25
+ # --- SUITE DE INFRAESTRUCTURA ---
26
+ class InfrastructureTest < Minitest::Test
27
+ include IntegrationHelper
28
+
29
+ def setup
30
+ skip "RabbitMQ no disponible" unless IntegrationHelper.rabbitmq_available?
31
+ @queue = "test_infra_queue_#{SecureRandom.hex(4)}"
32
+ @exchange = "test_infra_exchange"
33
+ end
34
+
35
+ def test_00_worker_lifecycle
36
+ with_running_worker(queue: @queue, exchange: @exchange) do
37
+ assert true, "El worker levantó y cedió el control al bloque"
38
+ end
39
+ end
40
+
41
+ def test_01_dynamic_namespace_resolution
42
+ BugBunny.configure do |c|
43
+ c.controller_namespace = 'InfraTest'
44
+ end
45
+
46
+ with_running_worker(queue: @queue, exchange: @exchange) do
47
+ # Enviamos GET ping/123 -> InfraTest::PingController#show
48
+ resource = InfraResource.find('123')
49
+
50
+ # Verificamos que volvió el objeto construido
51
+ assert_equal '123', resource.id
52
+ assert_equal 'InfraTest', resource.namespace
53
+ assert_equal 'pong', resource.message
54
+ end
55
+
56
+ ensure
57
+ BugBunny.configure do |c|
58
+ c.controller_namespace = 'Rabbit::Controllers'
59
+ end
60
+ end
61
+ end