bug_bunny 3.0.5 → 3.1.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.
data/lib/bug_bunny.rb CHANGED
@@ -1,113 +1,104 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bunny'
2
4
  require 'logger'
3
- require 'connection_pool'
4
-
5
5
  require_relative 'bug_bunny/version'
6
- require_relative 'bug_bunny/config'
7
6
  require_relative 'bug_bunny/exception'
8
- require_relative 'bug_bunny/request'
9
- require_relative 'bug_bunny/session'
10
- require_relative 'bug_bunny/producer'
11
- require_relative 'bug_bunny/client'
12
- require_relative 'bug_bunny/resource'
13
- require_relative 'bug_bunny/rabbit'
14
- require_relative 'bug_bunny/consumer'
15
- require_relative 'bug_bunny/controller'
7
+ require_relative 'bug_bunny/configuration'
16
8
  require_relative 'bug_bunny/middleware/base'
17
9
  require_relative 'bug_bunny/middleware/stack'
18
10
  require_relative 'bug_bunny/middleware/raise_error'
19
11
  require_relative 'bug_bunny/middleware/json_response'
12
+ require_relative 'bug_bunny/client'
13
+ require_relative 'bug_bunny/session'
14
+ require_relative 'bug_bunny/consumer'
15
+ require_relative 'bug_bunny/request'
16
+ require_relative 'bug_bunny/producer'
17
+ require_relative 'bug_bunny/resource'
18
+ require_relative 'bug_bunny/controller'
19
+ require_relative 'bug_bunny/railtie' if defined?(Rails)
20
20
 
21
- # Punto de entrada principal y Namespace de la gema BugBunny.
22
- #
23
- # BugBunny es un framework ligero sobre RabbitMQ diseñado para simplificar
24
- # patrones de mensajería (RPC y Fire-and-Forget) en aplicaciones Ruby on Rails.
25
- #
26
- # @see BugBunny::Client Para enviar mensajes.
27
- # @see BugBunny::Resource Para mapear modelos remotos.
28
- # @see BugBunny::Consumer Para procesar mensajes entrantes.
21
+ # Módulo principal de la gema BugBunny.
22
+ # Actúa como espacio de nombres y punto de configuración global.
29
23
  module BugBunny
30
- # Factory method (Alias) para instanciar un nuevo Cliente.
31
- #
32
- # @param args [Hash] Argumentos pasados al constructor de {BugBunny::Client}.
33
- # @return [BugBunny::Client] Una nueva instancia del cliente.
34
- def self.new(**args)
35
- BugBunny::Client.new(**args)
24
+ class << self
25
+ # @return [BugBunny::Configuration] La configuración global actual.
26
+ attr_accessor :configuration
27
+
28
+ # @return [Bunny::Session, nil] La conexión global (Singleton) usada por procesos Rails.
29
+ attr_accessor :global_connection
36
30
  end
37
31
 
38
- # Configura la librería globalmente.
39
- #
40
- # @example Configuración típica en un initializer
41
- # BugBunny.configure do |config|
42
- # config.host = 'localhost'
43
- # config.username = 'guest'
44
- # config.rpc_timeout = 5
45
- # end
32
+ # Configura la librería BugBunny.
33
+ # Si no se ha configurado previamente, inicializa una nueva configuración por defecto.
46
34
  #
47
- # @yield [config] Bloque de configuración.
48
- # @yieldparam config [BugBunny::Config] Objeto de configuración global.
49
- # @return [BugBunny::Config] La configuración actualizada.
35
+ # @yieldparam config [BugBunny::Configuration] El objeto de configuración para modificar.
36
+ # @return [void]
50
37
  def self.configure
51
- self.configuration ||= Config.new
38
+ self.configuration ||= Configuration.new
52
39
  yield(configuration)
53
40
  end
54
41
 
55
- # Accesor al objeto de configuración global (Singleton).
42
+ # Crea e inicia una nueva conexión a RabbitMQ utilizando la gema Bunny.
43
+ # Mezcla las opciones pasadas explícitamente con la configuración global por defecto.
44
+ #
45
+ # @param options [Hash] Opciones de conexión que sobrescriben la configuración global.
46
+ # @option options [String] :host ('127.0.0.1') Host del servidor RabbitMQ.
47
+ # @option options [Integer] :port (5672) Puerto del servidor.
48
+ # @option options [String] :username ('guest') Usuario de conexión.
49
+ # @option options [String] :password ('guest') Contraseña.
50
+ # @option options [String] :vhost ('/') Virtual Host.
51
+ # @option options [Logger] :logger Logger para la conexión interna de Bunny.
52
+ # @option options [Boolean] :automatically_recover (true) Si debe reconectar automáticamente.
53
+ # @option options [Integer] :connection_timeout (10) Tiempo de espera para conectar.
54
+ # @option options [Integer] :read_timeout (10) Tiempo de espera para lectura.
55
+ # @option options [Integer] :write_timeout (10) Tiempo de espera para escritura.
56
+ # @option options [Integer] :heartbeat (15) Intervalo de latidos en segundos.
57
+ # @option options [Integer] :continuation_timeout (15000) Timeout para operaciones RPC internas.
56
58
  #
57
- # @return [BugBunny::Config] La instancia de configuración actual.
58
- def self.configuration
59
- @configuration ||= Config.new
59
+ # @return [Bunny::Session] Una sesión de Bunny ya iniciada (`start` ya invocado).
60
+ # @raise [Bunny::TCPConnectionFailed] Si no se puede conectar al servidor.
61
+ def self.create_connection(**options)
62
+ conn_options = merge_connection_options(options)
63
+ Bunny.new(conn_options).tap(&:start)
60
64
  end
61
65
 
62
- # Cierra la conexión global mantenida por {BugBunny::Rabbit}.
63
- # Útil para liberar recursos en scripts o tareas Rake al finalizar.
66
+ # Cierra la conexión global si existe.
67
+ #
68
+ # Este método es utilizado principalmente por el Railtie para asegurar que
69
+ # los procesos hijos (forks) de servidores como Puma o Spring no hereden
70
+ # la conexión TCP del proceso padre, forzando una reconexión limpia ("Lazy").
64
71
  #
65
- # @see BugBunny::Rabbit.disconnect
66
72
  # @return [void]
67
73
  def self.disconnect
68
- BugBunny::Rabbit.disconnect
69
- end
74
+ return unless @global_connection
70
75
 
71
- # Crea una nueva conexión a RabbitMQ (Bunny Session).
72
- #
73
- # Este método fusiona la configuración global por defecto con las opciones
74
- # pasadas explícitamente como argumentos, dando prioridad a estas últimas.
75
- #
76
- # Maneja automáticamente el inicio de la conexión (`start`) y captura errores
77
- # de red comunes envolviéndolos en excepciones de BugBunny.
78
- #
79
- # @param options [Hash] Opciones de conexión que sobrescriben la configuración global.
80
- # @option options [String] :host Host de RabbitMQ.
81
- # @option options [String] :vhost Virtual Host.
82
- # @option options [String] :username Usuario.
83
- # @option options [String] :password Contraseña.
84
- # @option options [Logger] :logger Logger personalizado.
85
- # @option options [Boolean] :automatically_recover (true/false).
86
- # @option options [Integer] :network_recovery_interval Intervalo de reconexión.
87
- # @option options [Integer] :connection_timeout Timeout de conexión TCP.
88
- # @return [Bunny::Session] Una sesión de Bunny iniciada y lista para usar.
89
- # @raise [BugBunny::CommunicationError] Si no se puede establecer la conexión TCP.
90
- def self.create_connection(**options)
91
- default = configuration
76
+ @global_connection.close if @global_connection.open?
77
+ @global_connection = nil
78
+ configuration.logger.info('[BugBunny] Global connection closed.')
79
+ end
92
80
 
93
- bunny = Bunny.new(
94
- host: options[:host] || default.host,
95
- username: options[:username] || default.username,
96
- password: options[:password] || default.password,
97
- vhost: options[:vhost] || default.vhost,
98
- logger: options[:logger] || default.bunny_logger,
99
- automatically_recover: options[:automatically_recover] || default.automatically_recover,
100
- network_recovery_interval: options[:network_recovery_interval] || default.network_recovery_interval,
101
- connection_timeout: options[:connection_timeout] || default.connection_timeout,
102
- read_timeout: options[:read_timeout] || default.read_timeout,
103
- write_timeout: options[:write_timeout] || default.write_timeout,
104
- heartbeat: options[:heartbeat] || default.heartbeat,
105
- continuation_timeout: options[:continuation_timeout] || default.continuation_timeout
106
- )
81
+ # @api private
82
+ # Fusiona las opciones del usuario con los valores por defecto de la configuración.
83
+ def self.merge_connection_options(options)
84
+ # .compact elimina los valores nil de options para no sobrescribir los defaults
85
+ default_connection_options.merge(options.compact)
86
+ end
107
87
 
108
- bunny.start
109
- bunny
110
- rescue Timeout::Error, Bunny::ConnectionError => e
111
- raise BugBunny::CommunicationError, e.message
88
+ # @api private
89
+ # Genera el hash de opciones por defecto basado en la configuración global.
90
+ # Extraído para reducir la métrica AbcSize de merge_connection_options.
91
+ def self.default_connection_options
92
+ cfg = configuration || Configuration.new
93
+ {
94
+ host: cfg.host, port: cfg.port,
95
+ username: cfg.username, password: cfg.password, vhost: cfg.vhost,
96
+ logger: cfg.bunny_logger, automatically_recover: cfg.automatically_recover,
97
+ connection_timeout: cfg.connection_timeout, read_timeout: cfg.read_timeout,
98
+ write_timeout: cfg.write_timeout, heartbeat: cfg.heartbeat,
99
+ continuation_timeout: cfg.continuation_timeout
100
+ }
112
101
  end
102
+
103
+ private_class_method :merge_connection_options, :default_connection_options
113
104
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_helper'
4
+ require 'connection_pool'
5
+
6
+ class FireAndForgetTest < Minitest::Test
7
+ def setup
8
+ skip "RabbitMQ no disponible" unless TestHelper.rabbitmq_available?
9
+
10
+ # 1. Configuración
11
+ BugBunny.configure do |c|
12
+ c.host = 'localhost'
13
+ c.username = 'wisproMQ'
14
+ c.password = 'wisproMQ'
15
+ c.port = 5672
16
+ c.logger = Logger.new(nil)
17
+ end
18
+
19
+ @pool = ConnectionPool.new(size: 1, timeout: 5) { BugBunny.create_connection }
20
+
21
+ # 2. Infraestructura de Test
22
+ @queue_name = 'test_fire_queue'
23
+ @exchange_name = 'test_fire_exchange'
24
+ @routing_key = 'logs.error'
25
+
26
+ # Usamos una Queue de Ruby para pasar el mensaje del Consumer al Test
27
+ @message_bucket = Queue.new
28
+
29
+ # 3. Consumidor "Espía"
30
+ @conn_consumer = BugBunny.create_connection
31
+ @consumer_thread = Thread.new do
32
+ ch = @conn_consumer.create_channel
33
+ # IMPORTANTE: Aquí declaramos el exchange como TOPIC
34
+ x = ch.topic(@exchange_name)
35
+ q = ch.queue(@queue_name).bind(x, routing_key: @routing_key)
36
+
37
+ q.subscribe(block: true) do |delivery_info, properties, body|
38
+ @message_bucket << {
39
+ body: body,
40
+ routing_key: delivery_info.routing_key
41
+ }
42
+ end
43
+ end
44
+ sleep 0.5 # Wait boot
45
+ end
46
+
47
+ def teardown
48
+ return unless @conn_consumer
49
+ @conn_consumer.close
50
+ @consumer_thread.kill
51
+ end
52
+
53
+ def test_publish_directly
54
+ client = BugBunny::Client.new(pool: @pool)
55
+ payload = { system: 'payment', error: 'timeout' }
56
+
57
+ # 1. Disparamos (Fire)
58
+ client.publish(
59
+ 'logs/error',
60
+ body: payload,
61
+ exchange: @exchange_name,
62
+ exchange_type: 'topic',
63
+ routing_key: @routing_key
64
+ )
65
+
66
+ # 2. Verificamos asíncronamente
67
+ received = nil
68
+ Timeout.timeout(2) do
69
+ received = @message_bucket.pop
70
+ end
71
+
72
+ # 3. Aserciones
73
+ assert_equal payload.to_json, received[:body]
74
+ assert_equal @routing_key, received[:routing_key]
75
+ end
76
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_helper'
4
+ require 'connection_pool'
5
+
6
+ # Controlador Dummy en memoria
7
+ module Rabbit
8
+ module Controllers
9
+ class IntegrationTest < BugBunny::Controller
10
+ def index
11
+ # CORRECCIÓN: Usar 'render' en lugar de retornar un hash implícito.
12
+ # Si solo retornas el valor, el Controller lo ignora y devuelve 204 (No Content).
13
+ render status: 200, json: { message: 'pong' }
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ class RpcFlowTest < Minitest::Test
20
+ def setup
21
+ skip "RabbitMQ no disponible" unless TestHelper.rabbitmq_available?
22
+
23
+ # 1. Configuración Real
24
+ BugBunny.configure do |c|
25
+ c.host = 'localhost'
26
+ c.username = 'wisproMQ'
27
+ c.password = 'wisproMQ'
28
+ c.port = 5672
29
+ c.logger = Logger.new(nil)
30
+ end
31
+
32
+ # 2. Pool para el Cliente
33
+ @pool = ConnectionPool.new(size: 1, timeout: 5) do
34
+ BugBunny.create_connection
35
+ end
36
+
37
+ # 3. Consumer en hilo separado
38
+ @conn_consumer = BugBunny.create_connection
39
+ @queue_name = 'test_integration_queue'
40
+ @exchange_name = 'test_integration_exchange'
41
+
42
+ @consumer_thread = Thread.new do
43
+ BugBunny::Consumer.subscribe(
44
+ connection: @conn_consumer,
45
+ queue_name: @queue_name,
46
+ exchange_name: @exchange_name,
47
+ routing_key: 'test_key',
48
+ block: true
49
+ )
50
+ end
51
+ sleep 0.5 # Esperar arranque
52
+ end
53
+
54
+ def teardown
55
+ return unless @conn_consumer
56
+ @conn_consumer.close
57
+ @consumer_thread.kill
58
+ end
59
+
60
+ def test_rpc_request_response
61
+ client = BugBunny::Client.new(pool: @pool)
62
+
63
+ # Usamos request() para RPC
64
+ response = client.request(
65
+ 'integration_test',
66
+ body: {},
67
+ exchange: @exchange_name,
68
+ routing_key: 'test_key'
69
+ )
70
+
71
+ # Ahora sí esperamos 200 OK
72
+ assert_equal 200, response['status']
73
+
74
+ body = response['body']
75
+ msg = body.is_a?(Hash) ? (body['message'] || body[:message]) : body
76
+ assert_equal 'pong', msg
77
+ end
78
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/setup'
3
+
4
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
5
+ require 'bug_bunny'
6
+
7
+ require 'minitest/autorun'
8
+ require 'mocha/minitest'
9
+ require 'logger'
10
+
11
+ BugBunny.configure do |config|
12
+ config.logger = Logger.new(nil)
13
+ config.bunny_logger = Logger.new(nil)
14
+ end
15
+
16
+ module TestHelper
17
+ def self.rabbitmq_available?
18
+ socket = TCPSocket.new('localhost', 5672)
19
+ socket.close
20
+ true
21
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
22
+ false
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_helper'
4
+
5
+ class ConfigurationTest < Minitest::Test
6
+ def setup
7
+ BugBunny.configuration = nil # Resetear singleton
8
+ end
9
+
10
+ def test_default_values
11
+ config = BugBunny::Configuration.new
12
+
13
+ assert_equal '127.0.0.1', config.host
14
+ assert_equal 5672, config.port # Validamos el fix de la v3.0.6
15
+ assert_equal 'guest', config.username
16
+ assert_equal '/', config.vhost
17
+ assert config.automatically_recover
18
+ end
19
+
20
+ def test_configure_block
21
+ BugBunny.configure do |c|
22
+ c.host = 'rabbit.prod'
23
+ c.port = 1234
24
+ end
25
+
26
+ assert_equal 'rabbit.prod', BugBunny.configuration.host
27
+ assert_equal 1234, BugBunny.configuration.port
28
+ end
29
+
30
+ def test_url_generation
31
+ config = BugBunny::Configuration.new
32
+ config.host = 'host'
33
+ config.port = 5672
34
+ config.username = 'u'
35
+ config.password = 'p'
36
+
37
+ # CAMBIO: Agregamos el slash extra al final que genera la interpolación
38
+ assert_equal "amqp://u:p@host:5672//", config.url
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_helper'
4
+
5
+ class ConsumerTest < Minitest::Test
6
+ def setup
7
+ @connection = mock('Bunny::Session')
8
+ @channel = mock('Bunny::Channel')
9
+
10
+ # Stubs para que Consumer.new no falle
11
+ @connection.stubs(:open?).returns(true)
12
+ @connection.stubs(:create_channel).returns(@channel)
13
+ @connection.stubs(:close)
14
+
15
+ # Stubs para Session y Channel
16
+ @channel.stubs(:confirm_select)
17
+ @channel.stubs(:prefetch)
18
+ @channel.stubs(:open?).returns(true)
19
+ @channel.stubs(:close)
20
+
21
+ @consumer = BugBunny::Consumer.new(@connection)
22
+ end
23
+
24
+ def test_router_dispatch
25
+ # Probamos la lógica interna del Router (método privado router_dispatch)
26
+
27
+ # Caso 1: POST /users -> Create
28
+ route_post = @consumer.send(:router_dispatch, 'POST', 'users')
29
+ assert_equal 'users', route_post[:controller]
30
+ assert_equal 'create', route_post[:action]
31
+
32
+ # Caso 2: GET /users/123 -> Show
33
+ route_show = @consumer.send(:router_dispatch, 'GET', 'users/123')
34
+ assert_equal 'users', route_show[:controller]
35
+ assert_equal 'show', route_show[:action]
36
+ assert_equal '123', route_show[:id]
37
+
38
+ # Caso 3: Custom Action (POST /users/1/promote)
39
+ route_custom = @consumer.send(:router_dispatch, 'POST', 'users/1/promote')
40
+ assert_equal 'users', route_custom[:controller]
41
+ assert_equal 'promote', route_custom[:action]
42
+ assert_equal '1', route_custom[:id]
43
+ end
44
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_helper'
4
+
5
+ class ControllerHeadersTest < Minitest::Test
6
+ # Controlador Dummy para probar headers
7
+ class HeadersController < BugBunny::Controller
8
+ def echo_headers
9
+ # 1. LEER del Request
10
+ client_token = headers['X-Client-Token']
11
+
12
+ # 2. ESCRIBIR en el Response
13
+ response_headers['X-Server-Time'] = '123456789'
14
+ response_headers['X-Received-Token'] = client_token
15
+
16
+ render status: 200, json: { message: 'ok' }
17
+ end
18
+ end
19
+
20
+ def test_headers_flow
21
+ # Simulamos los headers que enviaría el Consumer al Controller
22
+ request_headers = {
23
+ action: 'echo_headers',
24
+ 'X-Client-Token' => 'secret_abc'
25
+ }
26
+
27
+ # Ejecutamos el pipeline del controlador
28
+ response = HeadersController.call(headers: request_headers, body: {})
29
+
30
+ # Verificamos la estructura de la respuesta
31
+ assert_equal 200, response[:status]
32
+
33
+ # Verificamos que los headers de respuesta estén presentes
34
+ assert_kind_of Hash, response[:headers]
35
+ assert_equal '123456789', response[:headers]['X-Server-Time']
36
+ assert_equal 'secret_abc', response[:headers]['X-Received-Token']
37
+ end
38
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_helper'
4
+
5
+ class HybridResourceTest < Minitest::Test
6
+ # Definimos una clase temporal para el test
7
+ class Product < BugBunny::Resource
8
+ # Atributos explícitos (Tipados)
9
+ attribute :price, :decimal, default: 0.0
10
+ attribute :active, :boolean, default: false
11
+
12
+ # Validaciones mixtas
13
+ validates :price, numericality: { greater_than: 0 }
14
+ validates :name, presence: true # 'name' será dinámico
15
+ end
16
+
17
+ def test_hybrid_attributes_assignment
18
+ # Caso: Asignación mixta en initialize
19
+ p = Product.new(price: "10.5", name: "Laptop", active: "1")
20
+
21
+ # 1. Atributo Tipado (:decimal) -> Coerción automática
22
+ assert_kind_of BigDecimal, p.price
23
+ assert_equal 10.5, p.price
24
+
25
+ # 2. Atributo Tipado (:boolean) -> Coerción automática
26
+ assert_equal true, p.active
27
+
28
+ # 3. Atributo Dinámico (method_missing) -> String directo
29
+ assert_equal "Laptop", p.name
30
+ end
31
+
32
+ def test_hybrid_serialization
33
+ p = Product.new(price: 20.0, name: "Mouse")
34
+
35
+ # Simulamos serialización (lo que se enviaría a RabbitMQ)
36
+ payload = p.attributes_for_serialization
37
+
38
+ assert_equal 20.0, payload['price']
39
+ assert_equal "Mouse", payload['name']
40
+ assert_equal false, payload['active'] # Default value
41
+ end
42
+
43
+ def test_hybrid_dirty_tracking
44
+ p = Product.new(price: 10.0, name: "Old Name")
45
+ p.persisted = true
46
+ p.send(:clear_changes_information)
47
+
48
+ # Modificamos uno tipado y uno dinámico
49
+ p.price = 15.0
50
+ p.name = "New Name"
51
+
52
+ changes = p.changes_to_send
53
+
54
+ # Ambos deben aparecer en los cambios
55
+ assert_includes changes.keys, 'price'
56
+ assert_includes changes.keys, 'name'
57
+ assert_equal 15.0, changes['price']
58
+ assert_equal "New Name", changes['name']
59
+ end
60
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_helper'
4
+
5
+ class MiddlewareTest < Minitest::Test
6
+ # Middleware dummy para rastrear ejecución
7
+ class TrackerMiddleware < BugBunny::Middleware::Base
8
+ def on_request(env)
9
+ env[:trace] << 'req_in'
10
+ end
11
+
12
+ def on_complete(response)
13
+ response[:trace] << 'res_out'
14
+ end
15
+ end
16
+
17
+ def setup
18
+ @stack = BugBunny::Middleware::Stack.new
19
+ end
20
+
21
+ def test_execution_order
22
+ # La "App" final (Producer)
23
+ final_app = ->(env) {
24
+ env[:trace] << 'executing'
25
+ { body: 'ok', trace: env[:trace] }
26
+ }
27
+
28
+ @stack.use TrackerMiddleware
29
+ chain = @stack.build(final_app)
30
+
31
+ env = { trace: [] }
32
+ response = chain.call(env)
33
+
34
+ # El orden debe ser: Entrada -> Ejecución -> Salida (Cebolla)
35
+ expected_trace = ['req_in', 'executing', 'res_out']
36
+ assert_equal expected_trace, response[:trace]
37
+ end
38
+
39
+ def test_json_response_parsing
40
+ # Simulamos una respuesta JSON cruda
41
+ app = ->(_) { { 'body' => '{"foo":"bar"}', 'status' => 200 } }
42
+
43
+ middleware = BugBunny::Middleware::JsonResponse.new(app)
44
+ response = middleware.call({})
45
+
46
+ # Debe convertirse en Hash
47
+ assert_kind_of Hash, response['body']
48
+ assert_equal 'bar', response['body']['foo']
49
+ end
50
+
51
+ def test_raise_error_handling
52
+ # Simulamos un error 404
53
+ app = ->(_) { { 'status' => 404, 'body' => 'Not Found' } }
54
+
55
+ middleware = BugBunny::Middleware::RaiseError.new(app)
56
+
57
+ assert_raises(BugBunny::NotFound) do
58
+ middleware.call({})
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_helper'
4
+
5
+ class ConsumerTest < Minitest::Test
6
+ def setup
7
+ # CORRECCIÓN: Asegurar que existe configuración para evitar error en Session
8
+ BugBunny.configure do |c|
9
+ c.channel_prefetch = 1
10
+ end
11
+
12
+ @connection = mock('Bunny::Session')
13
+ @channel = mock('Bunny::Channel')
14
+
15
+ # Stubs para que Consumer.new no falle
16
+ @connection.stubs(:open?).returns(true)
17
+ @connection.stubs(:create_channel).returns(@channel)
18
+ @connection.stubs(:close)
19
+
20
+ # Stubs para Session y Channel
21
+ @channel.stubs(:confirm_select)
22
+ @channel.stubs(:prefetch)
23
+ @channel.stubs(:open?).returns(true)
24
+ @channel.stubs(:close)
25
+
26
+ @consumer = BugBunny::Consumer.new(@connection)
27
+ end
28
+
29
+ def test_router_dispatch
30
+ # Probamos la lógica interna del Router (método privado router_dispatch)
31
+
32
+ # Caso 1: POST /users -> Create
33
+ route_post = @consumer.send(:router_dispatch, 'POST', 'users')
34
+ assert_equal 'users', route_post[:controller]
35
+ assert_equal 'create', route_post[:action]
36
+
37
+ # Caso 2: GET /users/123 -> Show
38
+ route_show = @consumer.send(:router_dispatch, 'GET', 'users/123')
39
+ assert_equal 'users', route_show[:controller]
40
+ assert_equal 'show', route_show[:action]
41
+ assert_equal '123', route_show[:id]
42
+
43
+ # Caso 3: Custom Action (POST /users/1/promote)
44
+ route_custom = @consumer.send(:router_dispatch, 'POST', 'users/1/promote')
45
+ assert_equal 'users', route_custom[:controller]
46
+ assert_equal 'promote', route_custom[:action]
47
+ assert_equal '1', route_custom[:id]
48
+ end
49
+ end