hutch 0.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.
@@ -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
+