captainu-tincan 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tincan::Failure do
4
+ let(:dummy) do
5
+ instance = DummyClass.new
6
+ instance.name = 'Some Idiot'
7
+ instance
8
+ end
9
+
10
+ # let(:message_fixture) { IO.read('spec/fixtures/message.json').strip }
11
+ # let(:message) { Tincan::Message.from_json(message_fixture) }
12
+ let(:failure) { Tincan::Failure.new(100, 'some_queue') }
13
+ let(:fixture) { IO.read('spec/fixtures/failure.json').strip }
14
+
15
+ describe :initialize do
16
+ it 'takes an message object, sets attempt count to 0' do
17
+ expect(failure).to be_a(Tincan::Failure)
18
+ expect(failure.attempt_count).to eq(0)
19
+ expect(failure.failed_at).to be_a(DateTime)
20
+ end
21
+ end
22
+
23
+ describe :attempt_after do
24
+ it 'takes failed date/time and extends it by a number of seconds' do
25
+ failure.attempt_count = 1
26
+ expected = Rational(10, 86400)
27
+ expect(failure.attempt_after).to eq(failure.failed_at + expected)
28
+ end
29
+
30
+ it 'extends the next attempt based on attempt_count' do
31
+ failure.attempt_count = 2
32
+ expected = Rational(20, 86400)
33
+ expect(failure.attempt_after).to eq(failure.failed_at + expected)
34
+ end
35
+ end
36
+
37
+ describe :to_json do
38
+ it 'converts the failure to a serialized JSON string' do
39
+ expect(failure.to_json).to be_a(String)
40
+ expect { JSON.parse(failure.to_json) }.to_not raise_error
41
+ end
42
+ end
43
+
44
+ describe :from_json do
45
+ let(:from_json) { Tincan::Failure.from_json(fixture) }
46
+
47
+ it 'deserializes an object from a JSON string' do
48
+ expect(from_json).to be_a(Tincan::Failure)
49
+ end
50
+
51
+ it 'sets everything up properly' do
52
+ expect(from_json.message_id).to eq('55')
53
+ expect(from_json.attempt_count).to eq(1)
54
+ expect(from_json.failed_at).to be_a(DateTime)
55
+ expect(from_json.queue_name).to eq('data:object_one:client:messages')
56
+ end
57
+ end
58
+ end
@@ -0,0 +1 @@
1
+ {"failed_at":"2014-06-02T09:30:47-05:00","attempt_count":1,"message_id":"55","queue_name":"data:object_one:client:messages"}
@@ -0,0 +1 @@
1
+ {"object_name":"dummy_class","change_type":"create","object_data":{"name":"Some Idiot"},"published_at":"2014-06-02T09:43:36-05:00"}
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tincan::Message do
4
+ let(:dummy) do
5
+ instance = DummyClass.new
6
+ instance.name = 'Some Idiot'
7
+ instance
8
+ end
9
+
10
+ let(:message) do
11
+ Tincan::Message.new do |m|
12
+ m.object_name = 'dummy_class'
13
+ m.object_data = { name: dummy.name }
14
+ m.change_type = :create
15
+ end
16
+ end
17
+ let(:fixture) { IO.read('spec/fixtures/message.json').strip }
18
+
19
+ describe :initialize do
20
+ it 'takes an object and a change type symbol' do
21
+ expect(message).to be_a(Tincan::Message)
22
+ end
23
+
24
+ it 'fails with an invalid symbol type' do
25
+ process = -> { Tincan::Message.new(DummyClass.new, :bork) }
26
+ expect(process).to raise_error(ArgumentError)
27
+ end
28
+
29
+ it 'stores these as properties' do
30
+ dummy = DummyClass.new
31
+ msg = Tincan::Message.new do |m|
32
+ m.object_name = 'dummy_class'
33
+ m.object_data = { name: dummy.name }
34
+ m.change_type = :create
35
+ end
36
+ expect(msg.object_name).to eq('dummy_class')
37
+ expect(msg.change_type).to eq(:create)
38
+ expect(msg.object_data).to eq(name: dummy.name)
39
+ expect(msg.published_at).to be_a(DateTime)
40
+ end
41
+ end
42
+
43
+ describe :to_json do
44
+ it 'converts the message to a serialized JSON string' do
45
+ expect(message.to_json).to be_a(String)
46
+ expect { JSON.parse(message.to_json) }.to_not raise_error
47
+ end
48
+
49
+ it 'includes the proper data' do
50
+ fixture.slice!('"2014-06-02T09:43:36-05:00"}')
51
+ expect(message.to_json).to start_with(fixture)
52
+ end
53
+ end
54
+
55
+ describe :from_json do
56
+ let(:from_json) { Tincan::Message.from_json(fixture) }
57
+ it 'deserializes an object from a JSON string' do
58
+ expect(from_json).to be_a(Tincan::Message)
59
+ end
60
+
61
+ it 'sets everything up properly' do
62
+ expect(from_json.object_name).to eq('dummy_class')
63
+ expect(from_json.change_type).to eq(:create)
64
+ expect(from_json.object_data).to eq('name' => 'Some Idiot')
65
+ expect(from_json.published_at).to be_a(DateTime)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,202 @@
1
+ require 'spec_helper'
2
+
3
+ # A test helper for handling messages.
4
+ class Handler
5
+ attr_accessor :data, :context
6
+ end
7
+
8
+ describe Tincan::Receiver do
9
+ let(:fixture) { IO.read('spec/fixtures/message.json').strip }
10
+ let(:receiver) { Tincan::Receiver.new(options) }
11
+ let(:redis) { ::Redis.new(host: options[:redis_host]) }
12
+ let(:handler_one_alpha) { Handler.new }
13
+ let(:handler_one_beta) { Handler.new }
14
+ let(:handler_two_alpha) { Handler.new }
15
+ let(:exception_handler) { Handler.new }
16
+ let(:options) do
17
+ {
18
+ redis_host: 'localhost',
19
+ redis_port: 6379,
20
+ client_name: 'bork',
21
+ namespace: 'data',
22
+ logger: ::Logger.new(STDOUT),
23
+ listen_to: {
24
+ object_one: [
25
+ -> (data) { handler_one_alpha.data = data },
26
+ -> (data) { handler_one_beta.data = data }
27
+ ],
28
+ object_two: [-> (data) { handler_two_alpha.data = data }]
29
+ },
30
+ on_exception: (lambda do |ex, context|
31
+ exception_handler.data = ex
32
+ exception_handler.context = context
33
+ end)
34
+ }
35
+ end
36
+
37
+ before { redis.flushdb }
38
+
39
+ describe :lifecycle do
40
+ it 'can be setup with a block' do
41
+ instance = Tincan::Receiver.new do |config|
42
+ options.keys.each do |key|
43
+ config.send("#{key}=", options[key])
44
+ end
45
+ end
46
+
47
+ options.keys.each do |key|
48
+ expect(instance.send(key)).to eq(options[key])
49
+ end
50
+ end
51
+
52
+ it 'can be setup with an options hash' do
53
+ options.keys.each do |key|
54
+ expect(receiver.send(key)).to eq(options[key])
55
+ end
56
+ end
57
+ end
58
+
59
+ describe :related_objects do
60
+ it 'memoizes a redis client' do
61
+ expect(receiver.redis_client).to be_a(Redis)
62
+ expect(receiver.redis_client.client.host).to eq(receiver.redis_host)
63
+ expect(receiver.redis_client.client.port).to eq(receiver.redis_port)
64
+ end
65
+ end
66
+
67
+ describe :transactional_methods do
68
+ describe :register do
69
+ it 'registers itself as a receiver for supplied channels' do
70
+ receiver.register
71
+ receivers = redis.smembers('data:object_one:receivers')
72
+ expect(receivers).to include(receiver.client_name)
73
+ end
74
+
75
+ it 'returns self' do
76
+ expect(receiver.register).to eq(receiver)
77
+ end
78
+ end
79
+
80
+ describe :store_failure do
81
+ let(:failure) do
82
+ Tincan::Failure.new('55', 'data:object_one:client:messages')
83
+ end
84
+
85
+ it 'stores a message ID in a specialized failures list' do
86
+ failure.attempt_count = 1
87
+ receiver.store_failure(failure)
88
+ failures = redis.lrange('data:object_one:client:failures', 0, -1)
89
+ expected = '"attempt_count":1,"message_id":"55","queue_name":'
90
+ expected << '"data:object_one:client:messages"}'
91
+ expect(failures.first).to include(expected)
92
+ end
93
+
94
+ it 'returns the message count in the failures queue' do
95
+ result = receiver.store_failure(failure)
96
+ expect(result).to eq(1)
97
+
98
+ result = receiver.store_failure(failure)
99
+ expect(result).to eq(2)
100
+
101
+ failure.message_id = 100
102
+ result = receiver.store_failure(failure)
103
+ expect(result).to eq(3)
104
+ end
105
+ end
106
+ end
107
+
108
+ describe :message_handling_methods do
109
+ describe :handle_message_for_object do
110
+ it 'iterates through the channel dict and calls lambdas' do
111
+ msg = OpenStruct.new(object_data: 'hello world')
112
+ receiver.handle_message_for_object('object_one', msg)
113
+ msg2 = OpenStruct.new(object_data: 'goodbye world')
114
+ receiver.handle_message_for_object('object_two', msg2)
115
+
116
+ expect(handler_one_alpha.data).to eq(msg)
117
+ expect(handler_one_beta.data).to eq(msg)
118
+ expect(handler_two_alpha.data).to eq(msg2)
119
+ end
120
+ end
121
+ end
122
+
123
+ describe :loop_methods do
124
+ # These are very much "integration" tests as they test the main entry point
125
+ # and the conditions of the receiver from top to bottom.
126
+ describe :listen do
127
+ it 'registers, subscribes, calls methods, and DOES ALL THE THINGS' do
128
+ thread = Thread.new { receiver.listen }
129
+
130
+ get_receivers = -> { redis.smembers('data:object_one:receivers') }
131
+ expect(get_receivers).to eventually_equal(%w(bork))
132
+
133
+ redis.set('data:object_one:messages:1', fixture)
134
+ receivers = redis.smembers('data:object_one:receivers')
135
+ receivers.each do |receiver|
136
+ redis.rpush("data:object_one:#{receiver}:messages", '1')
137
+ end
138
+
139
+ message = Tincan::Message.from_json(fixture)
140
+ expect { handler_one_alpha.data }.to eventually_equal(message)
141
+ expect { handler_one_beta.data }.to eventually_equal(message)
142
+
143
+ thread.kill
144
+ end
145
+
146
+ xit 'calls a stored exception block on failure and keeps on ticking' do
147
+ pending 'Fails when part of an entire run, but not by itself.'
148
+ thread = Thread.new { receiver.listen }
149
+
150
+ get_receivers = -> { redis.smembers('data:object_one:receivers') }
151
+ expect(get_receivers).to eventually_equal(%w(bork))
152
+
153
+ bad_data = { name: 'this data sucks' }.to_json
154
+ redis.set('data:object_one:messages:2', bad_data)
155
+ receivers = redis.smembers('data:object_one:receivers')
156
+ receivers.each do |receiver|
157
+ redis.rpush("data:object_one:#{receiver}:messages", '2')
158
+ end
159
+
160
+ expect { exception_handler.data }.to eventually_be_a(NoMethodError)
161
+ expect { exception_handler.context }.to eventually_be_a(Hash)
162
+
163
+ thread.kill
164
+ end
165
+ end
166
+ end
167
+
168
+ describe :formatting_helper_methods do
169
+ describe :message_for_id do
170
+ let(:result) { receiver.message_for_id(1, 'bobsyouruncle') }
171
+ let(:message) { Tincan::Message.from_json(fixture) }
172
+ before do
173
+ redis.set('data:bobsyouruncle:messages:1', fixture)
174
+ end
175
+
176
+ it 'retrieves a message from Redis based on an ID and object' do
177
+ expect(result).to eq(message)
178
+ end
179
+
180
+ it 'is in the form of a Tincan::Message object' do
181
+ expect(result).to be_a(Tincan::Message)
182
+ end
183
+
184
+ it 'returns nil if the object was not found' do
185
+ redis.del('data:bobsyouruncle:messages:1')
186
+ expect(result).to be_nil
187
+ end
188
+ end
189
+
190
+ describe :message_list_keys do
191
+ it 'converts the listen_to ivar into properly-formatted Redis keys' do
192
+ expected = %w(one two).map do |i|
193
+ [
194
+ "data:object_#{i}:bork:messages",
195
+ "data:object_#{i}:bork:failures"
196
+ ]
197
+ end.flatten
198
+ expect(receiver.message_list_keys).to eq(expected)
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,121 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tincan::Sender do
4
+ let(:sender) { Tincan::Sender.new(options) }
5
+ let(:redis) { ::Redis.new(host: options[:redis_host]) }
6
+ let(:fixture) { IO.read('spec/fixtures/message.json').strip }
7
+ let(:message) { Tincan::Message.from_json(fixture) }
8
+ let(:options) do
9
+ {
10
+ redis_host: 'localhost',
11
+ redis_port: 6379,
12
+ namespace: 'data'
13
+ }
14
+ end
15
+
16
+ before { redis.flushdb }
17
+
18
+ describe :lifecycle do
19
+ it 'can be setup with a block' do
20
+ instance = Tincan::Sender.new do |config|
21
+ options.keys.each do |key|
22
+ config.send("#{key}=", options[key])
23
+ end
24
+ end
25
+
26
+ options.keys.each do |key|
27
+ expect(instance.send(key)).to eq(options[key])
28
+ end
29
+ end
30
+
31
+ it 'can be setup with an options hash' do
32
+ options.keys.each do |key|
33
+ expect(sender.send(key)).to eq(options[key])
34
+ end
35
+ end
36
+ end
37
+
38
+ describe :related_objects do
39
+ it 'memoizes a redis client' do
40
+ expect(sender.redis_client).to be_a(Redis)
41
+ expect(sender.redis_client.client.host).to eq(sender.redis_host)
42
+ expect(sender.redis_client.client.port).to eq(sender.redis_port)
43
+ end
44
+ end
45
+
46
+ describe :transactional_methods do
47
+ before { redis.sadd('data:object:receivers', 'some_client') }
48
+
49
+ describe :keys_for_receivers do
50
+ it 'grabs receivers from Redis, maps them into message list keys' do
51
+ result = sender.keys_for_receivers('object')
52
+ expect(result).to eq(%w(data:object:some_client:messages))
53
+ end
54
+ end
55
+
56
+ describe :flush_all_queues_for_object do
57
+ it 'deletes all receiver object keys using keys_for_receivers' do
58
+ redis.rpush('data:object:some_client:messages', 'message!!!')
59
+ expect(redis.llen('data:object:some_client:messages')).to eq(1)
60
+ sender.flush_all_queues_for_object('object')
61
+ expect(redis.llen('data:object:some_client:messages')).to eq(0)
62
+ result = redis.lpop('data:object:some_client:messages')
63
+ expect(result).to be_nil
64
+ end
65
+ end
66
+ end
67
+
68
+ describe :communication_methods do
69
+ # These are very much "integration" tests as they test the main entry point
70
+ # and the conditions of the sender from top to bottom.
71
+ describe :publish do
72
+ let(:dummy) do
73
+ instance = DummyClass.new
74
+ instance.name = 'Some Idiot'
75
+ instance
76
+ end
77
+
78
+ let(:message) do
79
+ Tincan::Message.new do |m|
80
+ m.object_name = 'dummy_class'
81
+ m.object_data = { name: dummy.name }
82
+ m.change_type = :create
83
+ end
84
+ end
85
+
86
+ before do
87
+ redis.sadd('data:dummy_class:receivers', 'some_client')
88
+ sender.publish(message)
89
+ @timestamp = Time.now.to_i
90
+ end
91
+
92
+ it 'publishes a message to Redis at a specific key' do
93
+ message = redis.get("data:dummy_class:messages:#{@timestamp}")
94
+ expect(message).to be_a(String)
95
+ expected = '{"object_name":"dummy_class","change_type":"create",'
96
+ expected += '"object_data":{"name":"Some Idiot"},"published_at":'
97
+ expect(message).to start_with(expected)
98
+ end
99
+
100
+ it 'also publishes a message ID to client-specific receiver lists' do
101
+ identifier = redis.lpop('data:dummy_class:some_client:messages')
102
+ expect(identifier).to eq(@timestamp.to_s)
103
+ end
104
+ end
105
+ end
106
+
107
+ describe :formatting_helper_methods do
108
+ describe :identifier_for_message do
109
+ it 'generates a timestamp from the passed-in message' do
110
+ expect(sender.identifier_for_message(message)).to eq(1401720216)
111
+ end
112
+ end
113
+
114
+ describe :primary_key_for_message do
115
+ it 'joins namespace, object name, and more to create a unique key' do
116
+ expected = 'data:dummy_class:messages:1401720216'
117
+ expect(sender.primary_key_for_message(message)).to eq(expected)
118
+ end
119
+ end
120
+ end
121
+ end