captainu-tincan 0.7.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,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