bug_bunny 4.6.1 → 4.8.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/.agents/skills/rabbitmq-expert/SKILL.md +1555 -0
- data/.claude/commands/gem-ai-setup.md +174 -0
- data/.claude/commands/pr.md +53 -0
- data/.claude/commands/release.md +52 -0
- data/.claude/commands/rubocop.md +22 -0
- data/.claude/commands/service-ai-setup.md +168 -0
- data/.claude/commands/test.md +28 -0
- data/.claude/commands/yard.md +46 -0
- data/CHANGELOG.md +50 -15
- data/CLAUDE.md +240 -0
- data/README.md +154 -221
- data/Rakefile +19 -3
- data/docs/_index.md +50 -0
- data/docs/ai/_index.md +56 -0
- data/docs/ai/antipatterns.md +166 -0
- data/docs/ai/api.md +251 -0
- data/docs/ai/architecture.md +92 -0
- data/docs/ai/errors.md +158 -0
- data/docs/ai/faq_external.md +133 -0
- data/docs/ai/faq_internal.md +86 -0
- data/docs/ai/glossary.md +45 -0
- data/docs/concepts.md +140 -0
- data/docs/howto/controller.md +194 -0
- data/docs/howto/middleware_client.md +119 -0
- data/docs/howto/middleware_consumer.md +127 -0
- data/docs/howto/rails.md +214 -0
- data/docs/howto/resource.md +200 -0
- data/docs/howto/routing.md +133 -0
- data/docs/howto/testing.md +259 -0
- data/docs/howto/tracing.md +119 -0
- data/lib/bug_bunny/client.rb +45 -21
- data/lib/bug_bunny/configuration.rb +63 -0
- data/lib/bug_bunny/consumer.rb +51 -37
- data/lib/bug_bunny/consumer_middleware.rb +14 -5
- data/lib/bug_bunny/controller.rb +39 -18
- data/lib/bug_bunny/exception.rb +5 -1
- data/lib/bug_bunny/middleware/raise_error.rb +3 -3
- data/lib/bug_bunny/observability.rb +28 -6
- data/lib/bug_bunny/producer.rb +11 -13
- data/lib/bug_bunny/railtie.rb +8 -7
- data/lib/bug_bunny/request.rb +3 -11
- data/lib/bug_bunny/resource.rb +81 -41
- data/lib/bug_bunny/routing/route.rb +6 -1
- data/lib/bug_bunny/routing/route_set.rb +60 -22
- data/lib/bug_bunny/session.rb +18 -11
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +4 -2
- data/lib/generators/bug_bunny/install/install_generator.rb +45 -5
- data/lib/tasks/bug_bunny.rake +50 -0
- data/plan_test.txt +63 -0
- data/skills-lock.json +10 -0
- data/spec/integration/client_spec.rb +117 -0
- data/spec/integration/consumer_middleware_spec.rb +86 -0
- data/spec/integration/controller_spec.rb +140 -0
- data/spec/integration/error_handling_spec.rb +57 -0
- data/spec/integration/infrastructure_spec.rb +52 -0
- data/spec/integration/resource_spec.rb +113 -0
- data/spec/spec_helper.rb +70 -0
- data/spec/support/bunny_mocks.rb +18 -0
- data/spec/support/integration_helper.rb +87 -0
- data/spec/unit/client_session_pool_spec.rb +159 -0
- data/spec/unit/configuration_spec.rb +164 -0
- data/spec/unit/consumer_middleware_spec.rb +129 -0
- data/spec/unit/consumer_spec.rb +90 -0
- data/spec/unit/controller_after_action_spec.rb +155 -0
- data/spec/unit/observability_spec.rb +167 -0
- data/spec/unit/resource_attributes_spec.rb +69 -0
- data/spec/unit/session_spec.rb +98 -0
- metadata +50 -3
- data/sig/bug_bunny.rbs +0 -4
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
namespace :bug_bunny do
|
|
6
|
+
desc 'Sync BugBunny AI docs reference in CLAUDE.md with the installed version'
|
|
7
|
+
task :sync do
|
|
8
|
+
spec = Gem::Specification.find_by_name('bug_bunny')
|
|
9
|
+
version = spec.version.to_s
|
|
10
|
+
docs_path = File.join(spec.gem_dir, 'docs', 'ai')
|
|
11
|
+
claude_md_path = File.join(Dir.pwd, 'CLAUDE.md')
|
|
12
|
+
|
|
13
|
+
content = if File.exist?(claude_md_path)
|
|
14
|
+
File.read(claude_md_path)
|
|
15
|
+
else
|
|
16
|
+
app_name = File.basename(Dir.pwd).split(/[-_]/).map(&:capitalize).join
|
|
17
|
+
puts 'bug_bunny:sync — CLAUDE.md not found, creating it.'
|
|
18
|
+
"# #{app_name}\n"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Idempotent: same version already present, nothing to do
|
|
22
|
+
if content.include?('### bug_bunny') && content.include?("**Version:** #{version}")
|
|
23
|
+
puts "bug_bunny:sync — already at #{version}, nothing to do."
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
block = <<~BLOCK
|
|
28
|
+
### bug_bunny
|
|
29
|
+
- **Version:** #{version}
|
|
30
|
+
- **Docs:** #{docs_path}
|
|
31
|
+
- **Updated:** #{Date.today}
|
|
32
|
+
BLOCK
|
|
33
|
+
|
|
34
|
+
# Replace existing block if present
|
|
35
|
+
if content.match?(/^### bug_bunny\n/)
|
|
36
|
+
updated = content.gsub(/^### bug_bunny\n(?:- \*\*.*\n)*/, block)
|
|
37
|
+
File.write(claude_md_path, updated)
|
|
38
|
+
puts "bug_bunny:sync — updated to #{version} in CLAUDE.md"
|
|
39
|
+
elsif content.include?('## Gemas internas')
|
|
40
|
+
# Append under existing section
|
|
41
|
+
updated = content.sub(/^## Gemas internas\n/, "## Gemas internas\n\n#{block}")
|
|
42
|
+
File.write(claude_md_path, updated)
|
|
43
|
+
puts "bug_bunny:sync — added #{version} under '## Gemas internas' in CLAUDE.md"
|
|
44
|
+
else
|
|
45
|
+
# Create section at end of file
|
|
46
|
+
File.write(claude_md_path, content.rstrip + "\n\n## Gemas internas\n\n#{block}")
|
|
47
|
+
puts "bug_bunny:sync — added '## Gemas internas' section with #{version} to CLAUDE.md"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/plan_test.txt
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
📋 Plan de Testing: BugBunny v3.1 (Extendido)
|
|
2
|
+
🟢 Nivel 0: Infraestructura (Los Cimientos)
|
|
3
|
+
Test 0.0: test_helper capaz de levantar consumers con Exchanges Direct, Topic y Fanout.
|
|
4
|
+
|
|
5
|
+
Test 0.1: Control de "Namespace" dinámico en los tests para evitar colisión de nombres de clase.
|
|
6
|
+
|
|
7
|
+
🔵 Nivel 1: El Recurso (Resource ORM)
|
|
8
|
+
Test 1.1: CRUD Completo (Create, Find, Update) con Exchange Topic (Default).
|
|
9
|
+
|
|
10
|
+
Test 1.2: Manejo de Errores (404, 500, Timeout).
|
|
11
|
+
|
|
12
|
+
Test 1.3: Resource.where con filtros anidados (Query String).
|
|
13
|
+
|
|
14
|
+
🟠 Nivel 2: Cliente Manual (Publisher Mode)
|
|
15
|
+
Este nivel prueba el uso de la gema como un cliente RabbitMQ puro, sin la magia de Active Record.
|
|
16
|
+
|
|
17
|
+
Test 2.1 (Fire-and-Forget):
|
|
18
|
+
|
|
19
|
+
Usar client.publish('logs.error', body: { msg: 'Crash' }).
|
|
20
|
+
|
|
21
|
+
Verificar que el mensaje llega a la cola sin esperar respuesta.
|
|
22
|
+
|
|
23
|
+
Test 2.2 (RPC Manual):
|
|
24
|
+
|
|
25
|
+
Usar client.request('users/123', method: :get).
|
|
26
|
+
|
|
27
|
+
Verificar que devuelve un hash parseado y maneja el timeout.
|
|
28
|
+
|
|
29
|
+
Test 2.3 (Raw Bytes):
|
|
30
|
+
|
|
31
|
+
Enviar un string crudo (no JSON) y verificar que llega intacto.
|
|
32
|
+
|
|
33
|
+
🟣 Nivel 3: Topologías de Exchange (Routing Complejo)
|
|
34
|
+
Aquí probamos que la gema respeta las reglas de RabbitMQ.
|
|
35
|
+
|
|
36
|
+
Test 3.1 (Topic Wildcards):
|
|
37
|
+
|
|
38
|
+
Worker escucha en notifications.*.
|
|
39
|
+
|
|
40
|
+
Cliente envía a notifications.email y notifications.sms.
|
|
41
|
+
|
|
42
|
+
Verificar que ambos llegan.
|
|
43
|
+
|
|
44
|
+
Test 3.2 (Direct Strict):
|
|
45
|
+
|
|
46
|
+
Worker escucha en orders_queue (Direct).
|
|
47
|
+
|
|
48
|
+
Cliente envía con routing key incorrecta.
|
|
49
|
+
|
|
50
|
+
Verificar que NO llega (o se va a Dead Letter si hubiera).
|
|
51
|
+
|
|
52
|
+
Test 3.3 (Fanout Broadcast):
|
|
53
|
+
|
|
54
|
+
Levantar 2 Workers escuchando el mismo Exchange Fanout.
|
|
55
|
+
|
|
56
|
+
Cliente publica 1 mensaje.
|
|
57
|
+
|
|
58
|
+
Verificar que AMBOS workers reciben el mensaje.
|
|
59
|
+
|
|
60
|
+
🟡 Nivel 4: Observabilidad y Middlewares
|
|
61
|
+
Test 4.1 (Tracing): Inyección y propagación de correlation_id (Cliente Manual -> Worker).
|
|
62
|
+
|
|
63
|
+
Test 4.2 (Middleware Chain): Verificar que un middleware puede modificar el body antes de salir.
|
data/skills-lock.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'support/integration_helper'
|
|
5
|
+
|
|
6
|
+
module ClientSpec
|
|
7
|
+
class EchoController < BugBunny::Controller
|
|
8
|
+
def index
|
|
9
|
+
render status: 200, json: { received: params[:message], via: 'ClientSpec::EchoController' }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
RSpec.describe BugBunny::Client, :integration do
|
|
15
|
+
let(:queue) { unique('client_q') }
|
|
16
|
+
let(:exchange) { unique('client_x') }
|
|
17
|
+
let(:client) { described_class.new(pool: TEST_POOL) }
|
|
18
|
+
|
|
19
|
+
before { BugBunny.configure { |c| c.controller_namespace = 'ClientSpec' } }
|
|
20
|
+
after { BugBunny.configure { |c| c.controller_namespace = 'BugBunny::Controllers' } }
|
|
21
|
+
|
|
22
|
+
describe '#publish' do
|
|
23
|
+
context 'con exchange topic' do
|
|
24
|
+
it 'entrega el mensaje con el routing key correcto' do
|
|
25
|
+
with_spy_worker(queue: queue, exchange: exchange, exchange_type: 'topic', routing_key: 'logs.#') do |messages|
|
|
26
|
+
client.publish('logs.error', exchange: exchange, exchange_type: 'topic', body: { level: 'error' })
|
|
27
|
+
|
|
28
|
+
msg = wait_for_message(messages)
|
|
29
|
+
expect(msg[:routing_key]).to eq('logs.error')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context 'con exchange direct' do
|
|
35
|
+
it 'entrega el mensaje al routing key exacto' do
|
|
36
|
+
with_spy_worker(queue: queue, exchange: exchange, exchange_type: 'direct', routing_key: 'alerts') do |messages|
|
|
37
|
+
client.publish('alerts', exchange: exchange, exchange_type: 'direct', body: { alert: true })
|
|
38
|
+
|
|
39
|
+
msg = wait_for_message(messages)
|
|
40
|
+
expect(msg[:routing_key]).to eq('alerts')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
context 'con exchange fanout' do
|
|
46
|
+
it 'entrega el mensaje ignorando el routing key' do
|
|
47
|
+
with_spy_worker(queue: queue, exchange: exchange, exchange_type: 'fanout', routing_key: '') do |messages|
|
|
48
|
+
client.publish('cualquier.key', exchange: exchange, exchange_type: 'fanout', body: { data: 1 })
|
|
49
|
+
|
|
50
|
+
msg = wait_for_message(messages)
|
|
51
|
+
expect(msg[:routing_key]).to eq('cualquier.key')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe '#request (RPC)' do
|
|
58
|
+
context 'con exchange topic' do
|
|
59
|
+
it 'retorna la respuesta del controlador' do
|
|
60
|
+
with_running_worker(queue: queue, exchange: exchange, exchange_type: 'topic', routing_key: 'echo') do
|
|
61
|
+
response = client.request('echo',
|
|
62
|
+
method: :get, exchange: exchange, exchange_type: 'topic',
|
|
63
|
+
body: { message: 'hello_topic' })
|
|
64
|
+
|
|
65
|
+
expect(response['status']).to eq(200)
|
|
66
|
+
expect(response['body']['received']).to eq('hello_topic')
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context 'con exchange direct' do
|
|
72
|
+
it 'retorna la respuesta del controlador' do
|
|
73
|
+
with_running_worker(queue: queue, exchange: exchange, exchange_type: 'direct', routing_key: 'echo') do
|
|
74
|
+
response = client.request('echo',
|
|
75
|
+
method: :get, routing_key: 'echo',
|
|
76
|
+
exchange: exchange, exchange_type: 'direct',
|
|
77
|
+
body: { message: 'hello_direct' })
|
|
78
|
+
|
|
79
|
+
expect(response['status']).to eq(200)
|
|
80
|
+
expect(response['body']['received']).to eq('hello_direct')
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
context 'con exchange fanout' do
|
|
86
|
+
it 'retorna la respuesta del controlador' do
|
|
87
|
+
with_running_worker(queue: queue, exchange: exchange, exchange_type: 'fanout', routing_key: '') do
|
|
88
|
+
response = client.request('echo',
|
|
89
|
+
method: :get, exchange: exchange, exchange_type: 'fanout',
|
|
90
|
+
body: { message: 'hello_fanout' })
|
|
91
|
+
|
|
92
|
+
expect(response['status']).to eq(200)
|
|
93
|
+
expect(response['body']['received']).to eq('hello_fanout')
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
context 'con exchange_options personalizadas (cascada nivel 3)' do
|
|
99
|
+
it 'publica sin error PRECONDITION_FAILED' do
|
|
100
|
+
custom_x = unique('custom_x')
|
|
101
|
+
conn = BugBunny.create_connection
|
|
102
|
+
ch = conn.create_channel
|
|
103
|
+
ch.direct(custom_x, durable: true, auto_delete: true)
|
|
104
|
+
|
|
105
|
+
expect do
|
|
106
|
+
client.publish('key',
|
|
107
|
+
exchange: custom_x, exchange_type: 'direct',
|
|
108
|
+
exchange_options: { durable: true, auto_delete: true },
|
|
109
|
+
body: { test: true })
|
|
110
|
+
end.not_to raise_error
|
|
111
|
+
ensure
|
|
112
|
+
ch&.exchange_delete(custom_x) rescue nil
|
|
113
|
+
conn&.close rescue nil
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'support/integration_helper'
|
|
5
|
+
|
|
6
|
+
module MiddlewareSpec
|
|
7
|
+
class PingController < BugBunny::Controller
|
|
8
|
+
def index
|
|
9
|
+
render status: 200, json: { pong: true }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Middleware de prueba que registra las llamadas en un array compartido
|
|
15
|
+
class TrackingMiddleware < BugBunny::ConsumerMiddleware::Base
|
|
16
|
+
def self.calls
|
|
17
|
+
@calls ||= []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.reset!
|
|
21
|
+
@calls = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(delivery_info, properties, body)
|
|
25
|
+
self.class.calls << {
|
|
26
|
+
routing_key: delivery_info.routing_key,
|
|
27
|
+
headers: properties.headers
|
|
28
|
+
}
|
|
29
|
+
@app.call(delivery_info, properties, body)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
RSpec.describe 'Consumer Middleware Stack', :integration do
|
|
34
|
+
let(:queue) { unique('middleware_q') }
|
|
35
|
+
let(:exchange) { unique('middleware_x') }
|
|
36
|
+
let(:client) { BugBunny::Client.new(pool: TEST_POOL) }
|
|
37
|
+
|
|
38
|
+
before do
|
|
39
|
+
BugBunny.configure { |c| c.controller_namespace = 'MiddlewareSpec' }
|
|
40
|
+
TrackingMiddleware.reset!
|
|
41
|
+
BugBunny.consumer_middlewares.use TrackingMiddleware
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
after do
|
|
45
|
+
BugBunny.configure { |c| c.controller_namespace = 'BugBunny::Controllers' }
|
|
46
|
+
# Limpiamos el middleware para no afectar otros specs
|
|
47
|
+
BugBunny.configuration.instance_variable_set(:@consumer_middlewares,
|
|
48
|
+
BugBunny::ConsumerMiddleware::Stack.new)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'ejecuta el middleware antes de process_message' do
|
|
52
|
+
with_running_worker(queue: queue, exchange: exchange, routing_key: 'ping') do
|
|
53
|
+
client.request('ping', method: :get, exchange: exchange, exchange_type: 'topic', routing_key: 'ping')
|
|
54
|
+
|
|
55
|
+
expect(TrackingMiddleware.calls).not_to be_empty
|
|
56
|
+
expect(TrackingMiddleware.calls.first[:routing_key]).to eq('ping')
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'ejecuta el middleware para cada mensaje recibido' do
|
|
61
|
+
with_running_worker(queue: queue, exchange: exchange, routing_key: 'ping') do
|
|
62
|
+
3.times { client.request('ping', method: :get, exchange: exchange, exchange_type: 'topic', routing_key: 'ping') }
|
|
63
|
+
|
|
64
|
+
expect(TrackingMiddleware.calls.length).to eq(3)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe 'rpc_reply_headers' do
|
|
69
|
+
it 'inyecta headers en el reply del consumer' do
|
|
70
|
+
received_headers = nil
|
|
71
|
+
|
|
72
|
+
BugBunny.configuration.rpc_reply_headers = -> { { 'X-Test-Header' => 'from-consumer' } }
|
|
73
|
+
BugBunny.configuration.on_rpc_reply = ->(headers) { received_headers = headers }
|
|
74
|
+
|
|
75
|
+
with_running_worker(queue: queue, exchange: exchange, routing_key: 'ping') do
|
|
76
|
+
client.request('ping', method: :get, exchange: exchange, exchange_type: 'topic', routing_key: 'ping')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
expect(received_headers).not_to be_nil
|
|
80
|
+
expect(received_headers['X-Test-Header']).to eq('from-consumer')
|
|
81
|
+
ensure
|
|
82
|
+
BugBunny.configuration.rpc_reply_headers = nil
|
|
83
|
+
BugBunny.configuration.on_rpc_reply = nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'support/integration_helper'
|
|
5
|
+
|
|
6
|
+
module ControllerSpec
|
|
7
|
+
class PingController < BugBunny::Controller
|
|
8
|
+
before_action :set_user
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
render status: 200, json: { pong: true, user: @user }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show
|
|
15
|
+
render status: 200, json: { id: params[:id], user: @user }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create
|
|
19
|
+
render status: 201, json: { created: params[:name] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def set_user
|
|
25
|
+
@user = 'test_user'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class AroundController < BugBunny::Controller
|
|
30
|
+
around_action :wrap_with_timing
|
|
31
|
+
|
|
32
|
+
def index
|
|
33
|
+
render status: 200, json: { action: 'index' }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def wrap_with_timing
|
|
39
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
40
|
+
yield
|
|
41
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
42
|
+
# Solo verificamos que el around_action ejecutó el bloque
|
|
43
|
+
raise 'negative elapsed' if elapsed < 0
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class RescueController < BugBunny::Controller
|
|
48
|
+
rescue_from StandardError, with: :handle_error
|
|
49
|
+
|
|
50
|
+
def index
|
|
51
|
+
raise ArgumentError, 'something went wrong'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def handle_error(e)
|
|
57
|
+
render status: 422, json: { error: e.message }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class NodeController < BugBunny::Controller
|
|
62
|
+
def index
|
|
63
|
+
nodes = params[:q] ? [{ status: params[:q][:status] }] : []
|
|
64
|
+
render status: 200, json: nodes
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def show
|
|
68
|
+
render status: 200, json: { id: params[:id] }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create
|
|
72
|
+
render status: 201, json: { node: params[:name] }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def update
|
|
76
|
+
render status: 200, json: { id: params[:id], updated: true }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def destroy
|
|
80
|
+
render status: 200, json: { id: params[:id], deleted: true }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
RSpec.describe BugBunny::Controller, :integration do
|
|
86
|
+
let(:queue) { unique('controller_q') }
|
|
87
|
+
let(:exchange) { unique('controller_x') }
|
|
88
|
+
let(:client) { BugBunny::Client.new(pool: TEST_POOL) }
|
|
89
|
+
|
|
90
|
+
before { BugBunny.configure { |c| c.controller_namespace = 'ControllerSpec' } }
|
|
91
|
+
after { BugBunny.configure { |c| c.controller_namespace = 'BugBunny::Controllers' } }
|
|
92
|
+
|
|
93
|
+
describe 'before_action' do
|
|
94
|
+
it 'ejecuta el callback antes de la acción y expone la variable de instancia' do
|
|
95
|
+
with_running_worker(queue: queue, exchange: exchange) do
|
|
96
|
+
response = client.request('ping',
|
|
97
|
+
method: :get, exchange: exchange, exchange_type: 'topic', routing_key: 'ping')
|
|
98
|
+
|
|
99
|
+
expect(response['status']).to eq(200)
|
|
100
|
+
expect(response['body']['user']).to eq('test_user')
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe 'around_action' do
|
|
106
|
+
it 'envuelve la acción y la ejecuta correctamente' do
|
|
107
|
+
with_running_worker(queue: queue, exchange: exchange, routing_key: 'around') do
|
|
108
|
+
response = client.request('around',
|
|
109
|
+
method: :get, exchange: exchange, exchange_type: 'topic', routing_key: 'around')
|
|
110
|
+
|
|
111
|
+
expect(response['status']).to eq(200)
|
|
112
|
+
expect(response['body']['action']).to eq('index')
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
describe 'rescue_from' do
|
|
118
|
+
it 'captura la excepción y retorna la respuesta del handler' do
|
|
119
|
+
with_running_worker(queue: queue, exchange: exchange, routing_key: 'rescue') do
|
|
120
|
+
response = client.request('rescue',
|
|
121
|
+
method: :get, exchange: exchange, exchange_type: 'topic', routing_key: 'rescue')
|
|
122
|
+
|
|
123
|
+
expect(response['status']).to eq(422)
|
|
124
|
+
expect(response['body']['error']).to eq('something went wrong')
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe 'params desde query string' do
|
|
130
|
+
it 'parsea los params del path y los expone en el controlador' do
|
|
131
|
+
with_running_worker(queue: queue, exchange: exchange) do
|
|
132
|
+
response = client.request('ping/42',
|
|
133
|
+
method: :get, exchange: exchange, exchange_type: 'topic')
|
|
134
|
+
|
|
135
|
+
expect(response['status']).to eq(200)
|
|
136
|
+
expect(response['body']['id']).to eq('42')
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'support/integration_helper'
|
|
5
|
+
|
|
6
|
+
module ErrorSpec
|
|
7
|
+
class BoomController < BugBunny::Controller
|
|
8
|
+
def index
|
|
9
|
+
raise StandardError, 'boom interno'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
RSpec.describe 'Error handling', :integration do
|
|
15
|
+
let(:queue) { unique('error_q') }
|
|
16
|
+
let(:exchange) { unique('error_x') }
|
|
17
|
+
let(:client) { BugBunny::Client.new(pool: TEST_POOL) }
|
|
18
|
+
|
|
19
|
+
before { BugBunny.configure { |c| c.controller_namespace = 'ErrorSpec' } }
|
|
20
|
+
after { BugBunny.configure { |c| c.controller_namespace = 'BugBunny::Controllers' } }
|
|
21
|
+
|
|
22
|
+
describe '404 — ruta no encontrada' do
|
|
23
|
+
it 'retorna status 404 cuando no hay ruta para el path' do
|
|
24
|
+
with_running_worker(queue: queue, exchange: exchange, routing_key: 'boom') do
|
|
25
|
+
response = client.request('ruta_inexistente',
|
|
26
|
+
method: :get, exchange: exchange, exchange_type: 'topic', routing_key: 'boom')
|
|
27
|
+
|
|
28
|
+
expect(response['status']).to eq(404)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe '404 — controlador no encontrado' do
|
|
34
|
+
it 'retorna status 404 cuando el controlador no existe en el namespace' do
|
|
35
|
+
BugBunny.configure { |c| c.controller_namespace = 'NamespaceQueNoExiste' }
|
|
36
|
+
|
|
37
|
+
with_running_worker(queue: queue, exchange: exchange, routing_key: 'boom') do
|
|
38
|
+
response = client.request('boom',
|
|
39
|
+
method: :get, exchange: exchange, exchange_type: 'topic', routing_key: 'boom')
|
|
40
|
+
|
|
41
|
+
expect(response['status']).to eq(404)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe '500 — excepción en el controlador' do
|
|
47
|
+
it 'retorna status 500 cuando el controlador lanza una excepción no manejada' do
|
|
48
|
+
with_running_worker(queue: queue, exchange: exchange, routing_key: 'boom') do
|
|
49
|
+
response = client.request('boom',
|
|
50
|
+
method: :get, exchange: exchange, exchange_type: 'topic', routing_key: 'boom')
|
|
51
|
+
|
|
52
|
+
expect(response['status']).to eq(500)
|
|
53
|
+
expect(response['body']['error']).to eq('Internal Server Error')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'support/integration_helper'
|
|
5
|
+
|
|
6
|
+
# Controladores de prueba aislados en namespace propio
|
|
7
|
+
module InfraSpec
|
|
8
|
+
class PingController < BugBunny::Controller
|
|
9
|
+
def index
|
|
10
|
+
render status: 200, json: { message: 'pong', namespace: 'InfraSpec' }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
render status: 200, json: { id: params[:id], message: 'pong', namespace: 'InfraSpec' }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class InfraSpecResource < BugBunny::Resource
|
|
20
|
+
self.resource_name = 'ping'
|
|
21
|
+
self.exchange = 'infra_spec_exchange'
|
|
22
|
+
self.exchange_type = 'topic'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
RSpec.describe 'Infrastructure', :integration do
|
|
26
|
+
let(:queue) { unique('infra_q') }
|
|
27
|
+
let(:exchange) { 'infra_spec_exchange' }
|
|
28
|
+
|
|
29
|
+
before do
|
|
30
|
+
BugBunny.configure { |c| c.controller_namespace = 'InfraSpec' }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
after do
|
|
34
|
+
BugBunny.configure { |c| c.controller_namespace = 'BugBunny::Controllers' }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'levanta el worker y cede el control al bloque' do
|
|
38
|
+
with_running_worker(queue: queue, exchange: exchange) do
|
|
39
|
+
expect(true).to be(true)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'resuelve el namespace del controlador dinámicamente' do
|
|
44
|
+
with_running_worker(queue: queue, exchange: exchange) do
|
|
45
|
+
resource = InfraSpecResource.find('42')
|
|
46
|
+
|
|
47
|
+
expect(resource.id).to eq('42')
|
|
48
|
+
expect(resource.namespace).to eq('InfraSpec')
|
|
49
|
+
expect(resource.message).to eq('pong')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'support/integration_helper'
|
|
5
|
+
|
|
6
|
+
module ResourceSpec
|
|
7
|
+
class NodeController < BugBunny::Controller
|
|
8
|
+
def index
|
|
9
|
+
nodes = params[:q] ? [{ id: '1', status: params.dig(:q, :status) }] : [{ id: '1' }, { id: '2' }]
|
|
10
|
+
render status: 200, json: nodes
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
render status: 200, json: { id: params[:id], name: 'node-01' }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
render status: 201, json: { id: '99', name: params.dig(:node, :name) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def update
|
|
22
|
+
render status: 200, json: { id: params[:id], updated: true, name: params.dig(:node, 'name') }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def destroy
|
|
26
|
+
render status: 200, json: { id: params[:id], deleted: true }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class SpecNode < BugBunny::Resource
|
|
32
|
+
self.resource_name = 'node'
|
|
33
|
+
self.param_key = 'node'
|
|
34
|
+
self.exchange = 'resource_spec_exchange'
|
|
35
|
+
self.exchange_type = 'topic'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
RSpec.describe BugBunny::Resource, :integration do
|
|
39
|
+
let(:queue) { unique('resource_q') }
|
|
40
|
+
let(:exchange) { 'resource_spec_exchange' }
|
|
41
|
+
|
|
42
|
+
before { BugBunny.configure { |c| c.controller_namespace = 'ResourceSpec' } }
|
|
43
|
+
after { BugBunny.configure { |c| c.controller_namespace = 'BugBunny::Controllers' } }
|
|
44
|
+
|
|
45
|
+
describe '.find' do
|
|
46
|
+
it 'retorna el recurso por id' do
|
|
47
|
+
with_running_worker(queue: queue, exchange: exchange) do
|
|
48
|
+
node = SpecNode.find('1')
|
|
49
|
+
|
|
50
|
+
expect(node.id).to eq('1')
|
|
51
|
+
expect(node.name).to eq('node-01')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '.where' do
|
|
57
|
+
it 'retorna todos los recursos sin filtros' do
|
|
58
|
+
with_running_worker(queue: queue, exchange: exchange) do
|
|
59
|
+
nodes = SpecNode.where
|
|
60
|
+
|
|
61
|
+
expect(nodes).to be_an(Array)
|
|
62
|
+
expect(nodes.length).to eq(2)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'pasa los filtros como query params al controlador' do
|
|
67
|
+
with_running_worker(queue: queue, exchange: exchange) do
|
|
68
|
+
nodes = SpecNode.where(q: { status: 'active' })
|
|
69
|
+
|
|
70
|
+
expect(nodes).to be_an(Array)
|
|
71
|
+
expect(nodes.first.status).to eq('active')
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe '.create' do
|
|
77
|
+
it 'crea el recurso y retorna el objeto creado' do
|
|
78
|
+
with_running_worker(queue: queue, exchange: exchange) do
|
|
79
|
+
node = SpecNode.create(name: 'node-nuevo')
|
|
80
|
+
|
|
81
|
+
expect(node.id).to eq('99')
|
|
82
|
+
expect(node.name).to eq('node-nuevo')
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe '#update' do
|
|
88
|
+
it 'actualiza el recurso por id' do
|
|
89
|
+
with_running_worker(queue: queue, exchange: exchange) do
|
|
90
|
+
node = SpecNode.new(id: '1')
|
|
91
|
+
node.persisted = true
|
|
92
|
+
result = node.update(name: 'node-actualizado')
|
|
93
|
+
|
|
94
|
+
expect(result).to be(true)
|
|
95
|
+
expect(node.id).to eq('1')
|
|
96
|
+
expect(node.updated).to be(true)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
describe '#destroy' do
|
|
102
|
+
it 'elimina el recurso por id' do
|
|
103
|
+
with_running_worker(queue: queue, exchange: exchange) do
|
|
104
|
+
node = SpecNode.new(id: '1')
|
|
105
|
+
node.persisted = true
|
|
106
|
+
result = node.destroy
|
|
107
|
+
|
|
108
|
+
expect(result).to be(true)
|
|
109
|
+
expect(node.persisted?).to be(false)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|