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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +228 -4
- data/Rakefile +10 -6
- data/bin_worker.rb +16 -10
- data/lib/bug_bunny/{config.rb → configuration.rb} +37 -28
- data/lib/bug_bunny/consumer.rb +53 -25
- data/lib/bug_bunny/controller.rb +135 -91
- data/lib/bug_bunny/exception.rb +4 -0
- data/lib/bug_bunny/producer.rb +10 -3
- data/lib/bug_bunny/railtie.rb +14 -19
- data/lib/bug_bunny/resource.rb +69 -150
- data/lib/bug_bunny/session.rb +65 -44
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +76 -85
- data/test/integration/fire_and_forget_test.rb +76 -0
- data/test/integration/rpc_flow_test.rb +78 -0
- data/test/test_helper.rb +24 -0
- data/test/unit/configuration_test.rb +40 -0
- data/test/unit/consumer_test.rb +44 -0
- data/test/unit/controller_headers_test.rb +38 -0
- data/test/unit/hybrid_resource_test.rb +60 -0
- data/test/unit/middleware_test.rb +61 -0
- data/test/unit/resource_test.rb +49 -0
- metadata +40 -4
- data/lib/bug_bunny/rabbit.rb +0 -82
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/
|
|
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
|
-
#
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
# @
|
|
48
|
-
# @
|
|
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 ||=
|
|
38
|
+
self.configuration ||= Configuration.new
|
|
52
39
|
yield(configuration)
|
|
53
40
|
end
|
|
54
41
|
|
|
55
|
-
#
|
|
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 [
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
63
|
-
#
|
|
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
|
-
|
|
69
|
-
end
|
|
74
|
+
return unless @global_connection
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
data/test/test_helper.rb
ADDED
|
@@ -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
|