hutch 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,157 @@
1
+ require 'spec_helper'
2
+ require 'hutch/broker'
3
+
4
+ describe Hutch::Broker do
5
+ let(:config) { deep_copy(Hutch::Config.user_config) }
6
+ subject(:broker) { Hutch::Broker.new(config) }
7
+
8
+ describe '#connect' do
9
+ before { broker.stub(:set_up_amqp_connection) }
10
+ before { broker.stub(:set_up_api_connection) }
11
+ before { broker.stub(:disconnect) }
12
+
13
+ it 'sets up the amqp connection' do
14
+ broker.should_receive(:set_up_amqp_connection)
15
+ broker.connect
16
+ end
17
+
18
+ it 'sets up the api connection' do
19
+ broker.should_receive(:set_up_api_connection)
20
+ broker.connect
21
+ end
22
+
23
+ it 'does not disconnect' do
24
+ broker.should_not_receive(:disconnect)
25
+ broker.connect
26
+ end
27
+
28
+ context 'when given a block' do
29
+ it 'disconnects' do
30
+ broker.should_receive(:disconnect).once
31
+ broker.connect { }
32
+ end
33
+ end
34
+ end
35
+
36
+ describe '#set_up_amqp_connection', rabbitmq: true do
37
+ context 'with valid details' do
38
+ before { broker.set_up_amqp_connection }
39
+ after { broker.disconnect }
40
+
41
+ its(:connection) { should be_a Bunny::Session }
42
+ its(:channel) { should be_a Bunny::Channel }
43
+ its(:exchange) { should be_a Bunny::Exchange }
44
+ end
45
+
46
+ context 'when given invalid details' do
47
+ before { config[:mq_host] = 'notarealhost' }
48
+ let(:set_up_amqp_connection) { ->{ broker.set_up_amqp_connection } }
49
+
50
+ specify { set_up_amqp_connection.should raise_error }
51
+ end
52
+ end
53
+
54
+ describe '#set_up_api_connection', rabbitmq: true do
55
+ context 'with valid details' do
56
+ before { broker.set_up_api_connection }
57
+ after { broker.disconnect }
58
+
59
+ its(:api_client) { should be_a CarrotTop }
60
+ end
61
+
62
+ context 'when given invalid details' do
63
+ before { config[:mq_api_host] = 'notarealhost' }
64
+ after { broker.disconnect }
65
+ let(:set_up_api_connection) { ->{ broker.set_up_api_connection } }
66
+
67
+ specify { set_up_api_connection.should raise_error }
68
+ end
69
+ end
70
+
71
+ describe '#bindings', rabbitmq: true do
72
+ around { |example| broker.connect { example.run } }
73
+ subject { broker.bindings }
74
+
75
+ context 'with no bindings' do
76
+ its(:keys) { should_not include 'test' }
77
+ end
78
+
79
+ context 'with a binding' do
80
+ around do |example|
81
+ queue = broker.queue('test').bind(broker.exchange, routing_key: 'key')
82
+ example.run
83
+ queue.unbind(broker.exchange, routing_key: 'key').delete
84
+ end
85
+
86
+ it { should include({ 'test' => ['key'] }) }
87
+ end
88
+ end
89
+
90
+ describe '#bind_queue' do
91
+ around { |example| broker.connect { example.run } }
92
+ let(:routing_keys) { %w( a b c ) }
93
+ let(:queue) { double('Queue', bind: nil, unbind: nil, name: 'consumer') }
94
+ before { broker.stub(bindings: { 'consumer' => ['d'] }) }
95
+
96
+ it 'calls bind for each routing key' do
97
+ routing_keys.each do |key|
98
+ queue.should_receive(:bind).with(broker.exchange, routing_key: key)
99
+ end
100
+ broker.bind_queue(queue, routing_keys)
101
+ end
102
+
103
+ it 'calls unbind for each redundant existing binding' do
104
+ queue.should_receive(:unbind).with(broker.exchange, routing_key: 'd')
105
+ broker.bind_queue(queue, routing_keys)
106
+ end
107
+
108
+ context '(rabbitmq integration test)', rabbitmq: true do
109
+ let(:queue) { broker.queue('consumer') }
110
+ let(:routing_key) { 'key' }
111
+
112
+ before { broker.unstub(:bindings) }
113
+ before { queue.bind(broker.exchange, routing_key: 'redundant-key') }
114
+ after { queue.unbind(broker.exchange, routing_key: routing_key).delete }
115
+
116
+ it 'results in the correct bindings' do
117
+ broker.bind_queue(queue, [routing_key])
118
+ broker.bindings.should include({ queue.name => [routing_key] })
119
+ end
120
+ end
121
+ end
122
+
123
+ describe '#wait_on_threads' do
124
+ let(:thread) { double('Thread') }
125
+ before { broker.stub(work_pool_threads: threads) }
126
+
127
+ context 'when all threads finish within the timeout' do
128
+ let(:threads) { [double(join: thread), double(join: thread)] }
129
+ specify { expect(broker.wait_on_threads(1)).to be_true }
130
+ end
131
+
132
+ context 'when timeout expires for one thread' do
133
+ let(:threads) { [double(join: thread), double(join: nil)] }
134
+ specify { expect(broker.wait_on_threads(1)).to be_false }
135
+ end
136
+ end
137
+
138
+ describe '#publish' do
139
+ context 'with a valid connection' do
140
+ before { broker.set_up_amqp_connection }
141
+ after { broker.disconnect }
142
+
143
+ it 'publishes to the exchange' do
144
+ broker.exchange.should_receive(:publish).once
145
+ broker.publish('test.key', 'message')
146
+ end
147
+ end
148
+
149
+ context 'without a valid connection' do
150
+ it 'logs an error' do
151
+ broker.logger.should_receive(:error)
152
+ broker.publish('test.key', 'message')
153
+ end
154
+ end
155
+ end
156
+ end
157
+
@@ -0,0 +1,69 @@
1
+ require 'hutch/config'
2
+
3
+ describe Hutch::Config do
4
+ let(:new_value) { 'not-localhost' }
5
+
6
+ describe '.get' do
7
+ context 'for valid attributes' do
8
+ subject { Hutch::Config.get(:mq_host) }
9
+
10
+ context 'with no overridden value' do
11
+ it { should == 'localhost' }
12
+ end
13
+
14
+ context 'with an overridden value' do
15
+ before { Hutch::Config.stub(user_config: { mq_host: new_value }) }
16
+ it { should == new_value }
17
+ end
18
+ end
19
+
20
+ context 'for invalid attributes' do
21
+ let(:invalid_get) { ->{ Hutch::Config.get(:invalid_attr) } }
22
+ specify { invalid_get.should raise_error Hutch::UnknownAttributeError }
23
+ end
24
+ end
25
+
26
+ describe '.set' do
27
+ context 'for valid attributes' do
28
+ before { Hutch::Config.set(:mq_host, new_value) }
29
+ subject { Hutch::Config.user_config[:mq_host] }
30
+
31
+ context 'sets value in user config hash' do
32
+ it { should == new_value }
33
+ end
34
+ end
35
+
36
+ context 'for invalid attributes' do
37
+ let(:invalid_set) { ->{ Hutch::Config.set(:invalid_attr, new_value) } }
38
+ specify { invalid_set.should raise_error Hutch::UnknownAttributeError }
39
+ end
40
+ end
41
+
42
+ describe 'a magic getter' do
43
+ context 'for a valid attribute' do
44
+ it 'calls get' do
45
+ Hutch::Config.should_receive(:get).with(:mq_host)
46
+ Hutch::Config.mq_host
47
+ end
48
+ end
49
+
50
+ context 'for an invalid attribute' do
51
+ let(:invalid_getter) { ->{ Hutch::Config.invalid_attr } }
52
+ specify { invalid_getter.should raise_error NoMethodError }
53
+ end
54
+ end
55
+
56
+ describe 'a magic setter' do
57
+ context 'for a valid attribute' do
58
+ it 'calls set' do
59
+ Hutch::Config.should_receive(:set).with(:mq_host, new_value)
60
+ Hutch::Config.mq_host = new_value
61
+ end
62
+ end
63
+
64
+ context 'for an invalid attribute' do
65
+ let(:invalid_setter) { ->{ Hutch::Config.invalid_attr = new_value } }
66
+ specify { invalid_setter.should raise_error NoMethodError }
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hutch::Consumer do
4
+ around(:each) do |example|
5
+ isolate_constants do
6
+ example.run
7
+ end
8
+ end
9
+
10
+ let(:simple_consumer) do
11
+ unless defined? SimpleConsumer
12
+ class SimpleConsumer
13
+ include Hutch::Consumer
14
+ consume 'hutch.test1'
15
+ end
16
+ end
17
+ SimpleConsumer
18
+ end
19
+
20
+ let(:complex_consumer) do
21
+ unless defined? ComplexConsumer
22
+ class ComplexConsumer
23
+ include Hutch::Consumer
24
+ consume 'hutch.test1', 'hutch.test2'
25
+ end
26
+ end
27
+ ComplexConsumer
28
+ end
29
+
30
+ describe 'module inclusion' do
31
+ it 'registers the class as a consumer' do
32
+ Hutch.should_receive(:register_consumer) do |klass|
33
+ klass.should == simple_consumer
34
+ end
35
+
36
+ simple_consumer
37
+ end
38
+ end
39
+
40
+
41
+ describe '.consume' do
42
+ it 'saves the routing key to the consumer' do
43
+ simple_consumer.routing_keys.should include 'hutch.test1'
44
+ end
45
+
46
+ context 'with multiple routing keys' do
47
+ it 'registers the class once for each routing key' do
48
+ complex_consumer.routing_keys.should include 'hutch.test1'
49
+ complex_consumer.routing_keys.should include 'hutch.test2'
50
+ end
51
+ end
52
+
53
+ context 'when given the same routing key multiple times' do
54
+ subject { simple_consumer.routing_keys }
55
+ before { simple_consumer.consume 'hutch.test1' }
56
+ its(:length) { should == 1}
57
+ end
58
+ end
59
+
60
+ describe '.queue_name' do
61
+ it 'replaces module separators with colons' do
62
+ module Foo
63
+ class Bar
64
+ include Hutch::Consumer
65
+ end
66
+ end
67
+
68
+ Foo::Bar.queue_name.should == 'foo:bar'
69
+ end
70
+
71
+ it 'converts camelcase class names to snake case' do
72
+ class FooBarBAZ
73
+ include Hutch::Consumer
74
+ end
75
+
76
+ FooBarBAZ.queue_name.should == 'foo_bar_baz'
77
+ end
78
+ end
79
+ end
80
+
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hutch::ErrorHandlers::Logger do
4
+ let(:error_handler) { Hutch::ErrorHandlers::Logger.new }
5
+
6
+ describe '#handle' do
7
+ let(:error) { stub(message: "Stuff went wrong", class: "RuntimeError",
8
+ backtrace: ["line 1", "line 2"]) }
9
+
10
+ it "logs three separate lines" do
11
+ Hutch::Logging.logger.should_receive(:error).exactly(3).times
12
+ error_handler.handle("1", stub, error)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hutch::ErrorHandlers::Sentry do
4
+ let(:error_handler) { Hutch::ErrorHandlers::Sentry.new }
5
+
6
+ describe '#handle' do
7
+ let(:error) do
8
+ begin
9
+ raise "Stuff went wrong"
10
+ rescue RuntimeError => err
11
+ err
12
+ end
13
+ end
14
+
15
+ it "logs the error to Sentry" do
16
+ Raven.should_receive(:capture_exception).with(error)
17
+ error_handler.handle("1", stub, error)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hutch::Logging do
4
+ let(:dummy_object) do
5
+ class DummyObject
6
+ include Hutch::Logging
7
+ end
8
+ end
9
+
10
+ describe '#logger' do
11
+ context 'with the default logger' do
12
+ subject { Hutch::Logging.logger }
13
+
14
+ it { should be_instance_of(Logger) }
15
+ end
16
+
17
+ context 'with a custom logger' do
18
+ let(:dummy_logger) { mock("Dummy logger", warn: true, info: true) }
19
+ after { Hutch::Logging.setup_logger }
20
+
21
+ it "users the custom logger" do
22
+ Hutch::Logging.logger = dummy_logger
23
+ Hutch::Logging.logger.should == dummy_logger
24
+ end
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,35 @@
1
+ require 'hutch/message'
2
+
3
+ describe Hutch::Message do
4
+ let(:delivery_info) { double('Delivery Info') }
5
+ let(:props) { double('Properties') }
6
+ let(:body) {{ foo: 'bar' }}
7
+ let(:json_body) { MultiJson.dump(body) }
8
+ subject(:message) { Hutch::Message.new(delivery_info, props, json_body) }
9
+
10
+ its(:body) { should == body }
11
+
12
+ describe '[]' do
13
+ subject { message[:foo] }
14
+ it { should == 'bar' }
15
+ end
16
+
17
+ [:message_id, :timestamp].each do |method|
18
+ describe method.to_s do
19
+ it 'delegates to @properties' do
20
+ props.should_receive(method)
21
+ message.send(method)
22
+ end
23
+ end
24
+ end
25
+
26
+ [:routing_key, :exchange].each do |method|
27
+ describe method.to_s do
28
+ it 'delegates to @delivery_info' do
29
+ delivery_info.should_receive(method)
30
+ message.send(method)
31
+ end
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+ require 'hutch/worker'
3
+
4
+ describe Hutch::Worker do
5
+ before { Raven.as_null_object }
6
+ let(:consumer) { double('Consumer', routing_keys: %w( a b c ),
7
+ queue_name: 'consumer') }
8
+ let(:consumers) { [consumer, double('Consumer')] }
9
+ let(:broker) { Hutch::Broker.new }
10
+ subject(:worker) { Hutch::Worker.new(broker, consumers) }
11
+
12
+ describe '#setup_queues' do
13
+ it 'sets up queues for each of the consumers' do
14
+ consumers.each do |consumer|
15
+ worker.should_receive(:setup_queue).with(consumer)
16
+ end
17
+ worker.setup_queues
18
+ end
19
+ end
20
+
21
+ describe '#setup_queue' do
22
+ let(:queue) { double('Queue', bind: nil, subscribe: nil) }
23
+ before { worker.stub(consumer_queue: queue) }
24
+ before { broker.stub(queue: queue, bind_queue: nil) }
25
+
26
+ it 'creates a queue' do
27
+ broker.should_receive(:queue).with(consumer.queue_name).and_return(queue)
28
+ worker.setup_queue(consumer)
29
+ end
30
+
31
+ it 'binds the queue to each of the routing keys' do
32
+ broker.should_receive(:bind_queue).with(queue, %w( a b c ))
33
+ worker.setup_queue(consumer)
34
+ end
35
+
36
+ it 'sets up a subscription' do
37
+ queue.should_receive(:subscribe).with(ack: true)
38
+ worker.setup_queue(consumer)
39
+ end
40
+ end
41
+
42
+ describe '#handle_message' do
43
+ let(:payload) { '{}' }
44
+ let(:consumer_instance) { double('Consumer instance') }
45
+ let(:delivery_info) { double('Delivery Info', routing_key: '',
46
+ delivery_tag: 'dt') }
47
+ let(:properties) { double('Properties', message_id: nil) }
48
+ before { consumer.stub(new: consumer_instance) }
49
+ before { broker.stub(:ack) }
50
+
51
+ it 'passes the message to the consumer' do
52
+ consumer_instance.should_receive(:process).
53
+ with(an_instance_of(Hutch::Message))
54
+ worker.handle_message(consumer, delivery_info, properties, payload)
55
+ end
56
+
57
+ it 'acknowledges the message' do
58
+ consumer_instance.stub(:process)
59
+ broker.should_receive(:ack).with(delivery_info.delivery_tag)
60
+ worker.handle_message(consumer, delivery_info, properties, payload)
61
+ end
62
+
63
+ context 'when the consumer raises an exception' do
64
+ before { consumer_instance.stub(:process).and_raise('a consumer error') }
65
+
66
+ it 'logs the error' do
67
+ Hutch::Config[:error_handlers].each do |backend|
68
+ backend.should_receive(:handle)
69
+ end
70
+ worker.handle_message(consumer, delivery_info, properties, payload)
71
+ end
72
+
73
+ it 'acknowledges the message' do
74
+ broker.should_receive(:ack).with(delivery_info.delivery_tag)
75
+ worker.handle_message(consumer, delivery_info, properties, payload)
76
+ end
77
+ end
78
+ end
79
+ end
80
+