sqs_web 0.0.1

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,47 @@
1
+ <li class="job" id="message_<%= message.message_id %>">
2
+ <input type="checkbox" class="bulk_check" id="batch_action_item_<%= message.message_id %>" value="<%= message.message_id %>/<%= queue[:name] %>" name="message_collection[]" form="bulk_action_form"/>
3
+ <dl>
4
+ <dt>ID</dt>
5
+ <dd>
6
+ <a name="<%= message.message_id %>"></a>
7
+ <a href="#<%= message.message_id %>"><%=h message.message_id %></a>
8
+ <div class="controls">
9
+ <form action="<%= u("remove/#{queue[:name]}/#{message.message_id}") %>" method="post">
10
+ <%= csrf_token_tag %>
11
+ <input type="submit" value="Remove" />
12
+ </form>
13
+ or
14
+ <form action="<%= u("requeue/#{queue[:name]}/#{message.message_id}") %>" method="post">
15
+ <%= csrf_token_tag %>
16
+ <input type="submit" value="Move to Source Queue" />
17
+ </form>
18
+ </div>
19
+ </dd>
20
+ <% if message.attributes["ApproximateReceiveCount"] %>
21
+ <dt>Receive Count</dt>
22
+ <dd><%=h message.attributes["ApproximateReceiveCount"] %></dd>
23
+ <% end %>
24
+ <dt>Queue Name</dt>
25
+ <dd><%=h queue[:name] %></dd>
26
+ <dt>Origin Queue</dt>
27
+ <dd><%=h queue[:source_url] %></dd>
28
+ <dt>Message Body</dt>
29
+ <dd>
30
+ <pre><%=h message.body %></pre>
31
+ </dd>
32
+ <dt>Raw Message</dt>
33
+ <dd>
34
+ <div class="backtrace">
35
+ <pre><%=h message.inspect.to_yaml[0..100] + '...' %></pre>
36
+ </div>
37
+ <a class="backtrace" href="#">Toggle full message</a>
38
+ <div class="backtrace full hide">
39
+ <pre><%=h message.inspect.to_yaml %></pre>
40
+ </div>
41
+ </dd>
42
+ <dt>Enqueued At</dt>
43
+ <dd class="time">
44
+ <%=h Time.at(message.attributes["SentTimestamp"].to_i/1000).rfc822 %>
45
+ </dd>
46
+ </dl>
47
+ </li>
@@ -0,0 +1,17 @@
1
+ <h1>
2
+ Overview
3
+ </h1>
4
+ <p class="sub">
5
+ The list below shows an overview of messages in the SQS queues.
6
+ </p>
7
+ <table class="overview">
8
+ <tr>
9
+ <th>Queue Name</th>
10
+ <th>Visible Messages Count</th>
11
+ <th>In Flight Messages Count</th>
12
+ <th>Source Queue</th>
13
+ </tr>
14
+ <% @queues.each do |queue| %>
15
+ <%= partial :queue_stats, {:queue => queue, :stats => @stats[queue[:name]]} %>
16
+ <% end %>
17
+ </table>
@@ -0,0 +1,14 @@
1
+ <tr>
2
+ <td>
3
+ <%= queue[:name] %>
4
+ </td>
5
+ <td>
6
+ <%= stats[:visible] %>
7
+ </td>
8
+ <td>
9
+ <%= stats[:in_flight] %>
10
+ </td>
11
+ <td>
12
+ <%= queue[:source_url] || "N/A" %>
13
+ </td>
14
+ </tr>
@@ -0,0 +1,13 @@
1
+ <h1>Working</h1>
2
+ <p class="sub">
3
+ The list below contains jobs currently being processed.
4
+ </p>
5
+ <p class="sub">
6
+ <%= "Showing #{start} to #{start + per_page} of #{@all_jobs.count} working jobs." %>
7
+ </p>
8
+ <ul class="job">
9
+ <% @jobs.each do |job| %>
10
+ <%= partial :job, {:job => job} %>
11
+ <% end %>
12
+ </ul>
13
+ <%= partial :next_more, :start => start, :total_size => @all_jobs.count, :per_page => per_page %>
@@ -0,0 +1,7 @@
1
+ # Sinatra will automatically use Rails sessions if mounted within a Rails app.
2
+ #
3
+ # If you enable sessions in a Sinatra app, and then mount it in a Rails 4 app,
4
+ # Rack::Session will blow up because ActionDispatch::Request::Session does not
5
+ # implement #each.
6
+
7
+ SqsWeb.disable :sessions
data/lib/sqs_web.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'sqs_web/application/flash_message'
2
+ require 'sqs_web/application/navigation'
3
+ require 'sqs_web/application/controller_action'
4
+ require 'sqs_web/application/app'
5
+ require 'sqs_web/application/sqs_action'
6
+ require 'sqs_web/application/poller_action'
7
+ require 'sqs_web/application/exception_handle'
8
+ require 'sqs_web/railtie' if defined?(Rails)
9
+ SqsAction.initialize_aws
@@ -0,0 +1,369 @@
1
+ require 'support/rails_app'
2
+ require 'support/fake_sqs'
3
+
4
+ Capybara.app = RailsApp
5
+
6
+ RSpec.describe "Mounted in Rails Application", :sqs do
7
+
8
+ SOURCE_QUEUE_NAME = "TestSourceQueue"
9
+
10
+ DLQ_QUEUE_NAME = "TestSourceQueueDLQ"
11
+
12
+ let(:sqs) { Aws::SQS::Client.new(region: "us-east-1", credentials: Aws::Credentials.new("fake", "fake")) }
13
+
14
+ let(:source_queue_url) { sqs.get_queue_url(queue_name: SOURCE_QUEUE_NAME).queue_url }
15
+
16
+ let(:dlq_queue_url) { sqs.get_queue_url(queue_name: DLQ_QUEUE_NAME).queue_url }
17
+
18
+ before do
19
+ sqs.config.endpoint = $fake_sqs.uri
20
+ [SOURCE_QUEUE_NAME, DLQ_QUEUE_NAME].each{|queue_name| sqs.create_queue(queue_name: queue_name)}
21
+ dlq_arn = sqs.get_queue_attributes(queue_url: dlq_queue_url).attributes.fetch("QueueArn")
22
+ #Set DLQ
23
+ sqs.set_queue_attributes(
24
+ queue_url: source_queue_url,
25
+ attributes: {
26
+ "RedrivePolicy" => "{\"deadLetterTargetArn\":\"#{dlq_arn}\",\"maxReceiveCount\":10}"
27
+ }
28
+ )
29
+ SqsWeb.options[:queues] = [SOURCE_QUEUE_NAME, DLQ_QUEUE_NAME]
30
+ end
31
+
32
+ # basic smoke test all the tabs
33
+ %w(overview dlq_console).each do |tab|
34
+ specify "test_#{tab}" do
35
+ visit "/sqs/#{tab}"
36
+ expect(page.status_code).to eq 200
37
+ end
38
+ end
39
+
40
+ describe "Overview page" do
41
+ it "will show Visible Messages" do
42
+ default_messages
43
+
44
+ visit "/sqs/overview"
45
+
46
+ match_content(page, "#{SOURCE_QUEUE_NAME} 5 0 N/A")
47
+ match_content(page, "#{DLQ_QUEUE_NAME} 3 0 #{source_queue_url}")
48
+ end
49
+
50
+ specify "In Flight Messages" do
51
+ default_messages
52
+
53
+ receive_messages(source_queue_url, {count: 3})
54
+ receive_messages(dlq_queue_url, {count: 2})
55
+
56
+ visit "/sqs/overview"
57
+
58
+ match_content(page, "#{SOURCE_QUEUE_NAME} 2 3 N/A")
59
+ match_content(page, "#{DLQ_QUEUE_NAME} 1 2 #{source_queue_url}")
60
+ end
61
+
62
+ specify "should be default page" do
63
+ visit "/sqs"
64
+
65
+ expect(current_path).to eq "/sqs/overview"
66
+ end
67
+
68
+ specify "handle non existent queues" do
69
+ SqsWeb.options[:queues] = ["BOGUSQUEUE"]
70
+
71
+ visit "/sqs/overview"
72
+
73
+ match_content(page, "Aws::SQS::Errors::NonExistentQueue: BOGUSQUEUE")
74
+ end
75
+ end
76
+
77
+ describe "DLQ Console", js: true do
78
+
79
+ it "should only show unique entries for each message" do
80
+ message_ids = generate_messages(dlq_queue_url, 5).map{|c| c.message_id}
81
+
82
+ visit "/sqs/dlq_console"
83
+
84
+ message_ids.each{|message_id| expect(page.all("#message_#{message_id}").count).to eq 1}
85
+ end
86
+
87
+ it "should delete single message" do
88
+ messages = generate_messages(dlq_queue_url, 2)
89
+ deleted_message_id = messages.pop.message_id
90
+ retained_message_id = messages.pop.message_id
91
+
92
+ visit "/sqs/dlq_console"
93
+
94
+ first("#message_#{deleted_message_id}").hover
95
+
96
+ within("#message_#{deleted_message_id}") do
97
+ click_on "Remove"
98
+ end
99
+
100
+ success_message = "Message ID: #{deleted_message_id} in Queue #{DLQ_QUEUE_NAME} was successfully removed."
101
+ expect(first("#alert").text).to eq success_message
102
+
103
+ expect(page.all("#message_#{deleted_message_id}").count).to eq 0
104
+ expect(page.all("#message_#{retained_message_id}").count).to eq 1
105
+
106
+ visit "/sqs/overview"
107
+
108
+ match_content(page, "#{DLQ_QUEUE_NAME} 1 0 #{source_queue_url}")
109
+ end
110
+
111
+ it "should handle deleting single message that is already deleted" do
112
+ deleted_message_id = generate_messages(dlq_queue_url, 1).first.message_id
113
+
114
+ visit "/sqs/dlq_console"
115
+
116
+ sqs.purge_queue({ queue_url: dlq_queue_url })
117
+
118
+ first("#message_#{deleted_message_id}").hover
119
+
120
+ within("#message_#{deleted_message_id}") do
121
+ click_on "Remove"
122
+ end
123
+
124
+ error_message = "Message ID: #{deleted_message_id} in Queue #{DLQ_QUEUE_NAME} has already been deleted or is not visible."
125
+ expect(first("#alert").text).to eq error_message
126
+ end
127
+
128
+ it "should remove multiple selected messages" do
129
+ messages = generate_messages(dlq_queue_url, 6)
130
+ deleted_message_ids = messages.pop(4).map{|c| c.message_id}
131
+ retained_message_ids = messages.pop(2).map{|c| c.message_id}
132
+
133
+ visit "/sqs/dlq_console"
134
+
135
+ deleted_message_ids.each do |message_id|
136
+ first("#batch_action_item_#{message_id}").set(true)
137
+ end
138
+
139
+ click_on "Bulk Remove"
140
+
141
+ expect(first("#alert").text).to eq "Selected messages have been removed successfully."
142
+
143
+ deleted_message_ids.each{|message_id| expect(page.all("#message_#{message_id}").count).to eq 0}
144
+ retained_message_ids.each{|message_id| expect(page.all("#message_#{message_id}").count).to eq 1}
145
+
146
+ visit "/sqs/overview"
147
+
148
+ match_content(page, "#{DLQ_QUEUE_NAME} 2 0 #{source_queue_url}")
149
+ end
150
+
151
+ it "should handle removing multiple selected messages where one or more is already deleted or not visible" do
152
+ generate_messages(dlq_queue_url, 3)
153
+
154
+ visit "/sqs/dlq_console"
155
+
156
+ messages = receive_messages(dlq_queue_url, {count: 3}).messages
157
+ sqs.delete_message({queue_url: dlq_queue_url, receipt_handle: messages[2].receipt_handle})
158
+ sqs.change_message_visibility_batch({
159
+ queue_url: dlq_queue_url,
160
+ entries: messages.take(2).map do |message|
161
+ {id: message.message_id, receipt_handle: message.receipt_handle, visibility_timeout: 0}
162
+ end
163
+ })
164
+ messages.each do |message|
165
+ first("#batch_action_item_#{message.message_id}").set(true)
166
+ end
167
+
168
+ click_on "Bulk Remove"
169
+
170
+ expect(first("#alert").text).to eq "One or more messages may have already been removed or is not visible."
171
+
172
+ messages.each{|message| expect(page.all("#message_#{message.message_id}").count).to eq 0}
173
+
174
+ visit "/sqs/overview"
175
+
176
+ match_content(page, "#{DLQ_QUEUE_NAME} 0 0 #{source_queue_url}")
177
+ end
178
+
179
+ it "should handle clicking on Bulk Remove without any selection" do
180
+ messages = generate_messages(dlq_queue_url, 3)
181
+
182
+ visit "/sqs/dlq_console"
183
+
184
+ click_on "Bulk Remove"
185
+
186
+ expect(first("#alert").text).to eq ""
187
+
188
+ messages.each{|message| expect(page.all("#message_#{message.message_id}").count).to eq 1}
189
+ end
190
+
191
+ it "should render all information related to the visible messages" do
192
+ generate_messages(dlq_queue_url, 1)
193
+
194
+ visit "/sqs/dlq_console"
195
+
196
+ message = receive_messages(dlq_queue_url).messages.first
197
+ message.attributes["ApproximateReceiveCount"] = "1"
198
+
199
+ message_metadata = <<-EOF
200
+ ID #{message.message_id} or Receive Count 1
201
+ Queue Name #{DLQ_QUEUE_NAME} Origin Queue #{source_queue_url}
202
+ Message Body Test_0
203
+ EOF
204
+
205
+ message_entry = first("#message_#{message.message_id}")
206
+
207
+ within(message_entry) do
208
+ click_on "Toggle full message"
209
+ first(".toggle_format").click
210
+ end
211
+
212
+ match_content(message_entry, normalize_whitespace(message_metadata))
213
+ match_content(message_entry, normalize_whitespace(message.inspect.to_yaml.split('receipt_handle')[0]))
214
+ match_content(message_entry, normalize_whitespace(message.inspect.to_yaml.split('md5', 2)[1]))
215
+ match_content(message_entry, normalize_whitespace("Enqueued At #{Time.at(message.attributes["SentTimestamp"].to_i/1000).rfc822}"))
216
+ end
217
+
218
+ it "should not display any messages that are not in a DLQ" do
219
+ generate_messages(source_queue_url, 1)
220
+
221
+ visit "/sqs/dlq_console"
222
+
223
+ message = receive_messages(source_queue_url).messages.first
224
+
225
+ expect(first("#message_#{message.message_id}")).to be_nil
226
+
227
+ match_content(page, "Showing 0 visible messages")
228
+ end
229
+
230
+ it "should be able to move a single message to source queue" do
231
+ message_id = generate_messages(dlq_queue_url, 1).first.message_id
232
+
233
+
234
+ visit "/sqs/dlq_console"
235
+
236
+ first("#message_#{message_id}").hover
237
+
238
+ within("#message_#{message_id}") do
239
+ click_on "Move to Source Queue"
240
+ end
241
+
242
+ success_message = "Message ID: #{message_id} in Queue #{DLQ_QUEUE_NAME} was successfully moved to Source Queue #{source_queue_url}."
243
+ expect(first("#alert").text).to eq success_message
244
+ expect(page.all("#message_#{message_id}").count).to eq 0
245
+
246
+ visit "/sqs/overview"
247
+
248
+ match_content(page, "#{SOURCE_QUEUE_NAME} 1 0 N/A")
249
+ match_content(page, "#{DLQ_QUEUE_NAME} 0 0 #{source_queue_url}")
250
+
251
+ moved_message = receive_messages(source_queue_url).messages.first
252
+ expect(moved_message).to_not be_nil
253
+ expect(moved_message.body).to eq "Test_0"
254
+ expect(moved_message.attributes["ApproximateReceiveCount"]).to eq "1"
255
+ expect(moved_message.message_attributes["foo_class"].to_hash).to eq({string_value: "FooWorker", data_type: "String"})
256
+ end
257
+
258
+ it "should move multiple selected messages" do
259
+ messages = generate_messages(dlq_queue_url, 6)
260
+ retained_message_ids = messages.pop(2).map{|c| c.message_id}
261
+ moved_message_ids = messages.pop(4).map{|c| c.message_id}
262
+
263
+ visit "/sqs/dlq_console"
264
+
265
+ moved_message_ids.each do |message_id|
266
+ first("#batch_action_item_#{message_id}").set(true)
267
+ end
268
+
269
+ click_on "Bulk Move to Source Queue"
270
+
271
+ expect(first("#alert").text).to eq "Selected messages have been requeued successfully."
272
+
273
+ moved_message_ids.each{|message_id| expect(page.all("#message_#{message_id}").count).to eq 0}
274
+ retained_message_ids.each{|message_id| expect(page.all("#message_#{message_id}").count).to eq 1}
275
+
276
+ visit "/sqs/overview"
277
+
278
+ match_content(page, "#{SOURCE_QUEUE_NAME} 4 0 N/A")
279
+ match_content(page, "#{DLQ_QUEUE_NAME} 2 0 #{source_queue_url}")
280
+
281
+ moved_messages = receive_messages(source_queue_url, {count: 4}).messages.sort_by{|c| c.body}
282
+ moved_messages.each_with_index do |moved_message, index|
283
+ expect(moved_message).to_not be_nil
284
+ expect(moved_message.body).to match "Test_#{index}"
285
+ expect(moved_message.attributes["ApproximateReceiveCount"]).to eq "1"
286
+ expect(moved_message.message_attributes["foo_class"].to_hash).to eq({string_value: "FooWorker", data_type: "String"})
287
+ end
288
+ end
289
+
290
+ it "should handle moving multiple selected messages where one or more is already deleted or not visible" do
291
+ generate_messages(dlq_queue_url, 3)
292
+
293
+ visit "/sqs/dlq_console"
294
+
295
+ messages = receive_messages(dlq_queue_url, {count: 3}).messages
296
+ sqs.delete_message({queue_url: dlq_queue_url, receipt_handle: messages[2].receipt_handle})
297
+ sqs.change_message_visibility_batch({
298
+ queue_url: dlq_queue_url,
299
+ entries: messages.take(2).map do |message|
300
+ {id: message.message_id, receipt_handle: message.receipt_handle, visibility_timeout: 0}
301
+ end
302
+ })
303
+ messages.each do |message|
304
+ first("#batch_action_item_#{message.message_id}").set(true)
305
+ end
306
+
307
+ click_on "Bulk Move to Source Queue"
308
+
309
+ expect(first("#alert").text).to eq "One or more messages may have already been requeued or is not visible."
310
+
311
+ messages.each{|message| expect(page.all("#message_#{message.message_id}").count).to eq 0}
312
+
313
+ visit "/sqs/overview"
314
+
315
+ match_content(page, "#{SOURCE_QUEUE_NAME} 2 0 N/A")
316
+ match_content(page, "#{DLQ_QUEUE_NAME} 0 0 #{source_queue_url}")
317
+ end
318
+
319
+ it "should handle clicking on Bulk Move to Source Queue without any selection" do
320
+ messages = generate_messages(dlq_queue_url, 3)
321
+
322
+ visit "/sqs/dlq_console"
323
+
324
+ click_on "Bulk Move to Source Queue"
325
+
326
+ expect(first("#alert").text).to eq ""
327
+
328
+ messages.each{|message| expect(page.all("#message_#{message.message_id}").count).to eq 1}
329
+ end
330
+
331
+ it "should have a select/unselect all function" do
332
+ messages = generate_messages(dlq_queue_url, 3)
333
+
334
+ visit "/sqs/dlq_console"
335
+
336
+ first("#select_all").click
337
+
338
+ page.all(".bulk_check").each{|node| expect(node["checked"]).to eq true}
339
+
340
+ first("#select_all").click
341
+
342
+ page.all(".bulk_check").each{|node| expect(node["checked"]).to eq false}
343
+ end
344
+ end
345
+
346
+ def receive_messages(queue_url, options = {count: 1})
347
+ sqs.receive_message({
348
+ queue_url: queue_url,
349
+ attribute_names: ["All"],
350
+ message_attribute_names: ["All"],
351
+ max_number_of_messages: options[:count],
352
+ wait_time_seconds: 1,
353
+ visibility_timeout: options[:visibility_timeout]
354
+ })
355
+ end
356
+
357
+ def default_messages
358
+ generate_messages(source_queue_url, 5) + generate_messages(dlq_queue_url, 3)
359
+ end
360
+
361
+ def generate_messages(queue_url, count=1)
362
+ messages = []
363
+ count.times do |time|
364
+ messages << sqs.send_message(queue_url: queue_url, message_body: "Test_#{time}",
365
+ message_attributes: {"foo_class"=> {string_value: "FooWorker", data_type: "String"}})
366
+ end
367
+ messages
368
+ end
369
+ end
@@ -0,0 +1,28 @@
1
+ require "codeclimate-test-reporter"
2
+ require "byebug"
3
+ require 'capybara-webkit'
4
+ require 'capybara/rspec'
5
+ CodeClimate::TestReporter.start
6
+
7
+ ENV['RACK_ENV'] = 'development'
8
+
9
+ RSpec.configure do |config|
10
+ config.disable_monkey_patching!
11
+ config.include Capybara::DSL
12
+ end
13
+
14
+ Capybara.javascript_driver = :webkit
15
+
16
+ def match_content(actual, expected)
17
+ actual_text = actual.respond_to?(:text) ? normalize_whitespace(actual.text) : actual
18
+ expect(actual_text).to include(expected), <<-EOF
19
+ expected
20
+ "#{actual_text}"
21
+ to have content
22
+ "#{expected}"
23
+ EOF
24
+ end
25
+
26
+ def normalize_whitespace(content)
27
+ Capybara::Helpers.normalize_whitespace(content)
28
+ end
@@ -0,0 +1,19 @@
1
+ require "fake_sqs/test_integration"
2
+
3
+ db = ENV["SQS_DATABASE"] || ":memory:"
4
+ puts "\n\e[34mRunning specs with database \e[33m#{db}\e[0m"
5
+ $fake_sqs = FakeSQS::TestIntegration.new(
6
+ database: db,
7
+ sqs_endpoint: "localhost",
8
+ sqs_port: 4568,
9
+ )
10
+
11
+ SqsWeb.options[:aws][:access_key_id] = "fake"
12
+ SqsWeb.options[:aws][:secret_access_key] = "fake"
13
+ SqsWeb.options[:aws][:endpoint] = $fake_sqs.uri
14
+
15
+ RSpec.configure do |config|
16
+ config.before(:each, :sqs) { $fake_sqs.start }
17
+ config.before(:each, :sqs) { $fake_sqs.reset }
18
+ config.after(:suite) { $fake_sqs.stop }
19
+ end
@@ -0,0 +1,12 @@
1
+ require "action_controller/railtie"
2
+ require "logger"
3
+ require "sqs_web"
4
+
5
+ class RailsApp < Rails::Application
6
+ config.logger = Rails.logger = Logger.new($stdout)
7
+ config.secret_token = "a3d6cee7966878577a764ed273359d9e"
8
+
9
+ routes.draw do
10
+ match "/sqs" => SqsWeb, :anchor => false, via: [:get, :post]
11
+ end
12
+ end
data/sqs_web.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = "sqs_web"
3
+ gem.version = "0.0.1"
4
+ gem.author = "Nathaniel Ritholtz"
5
+ gem.email = "nritholtz@gmail.com"
6
+ gem.homepage = "https://github.com/nritholtz/sqs_web"
7
+ gem.summary = "Web interface for SQS inspired by delayed_job_web"
8
+ gem.description = gem.summary
9
+ gem.license = "MIT"
10
+
11
+ gem.executables = ["sqs_web"]
12
+
13
+ gem.files = [
14
+ "Gemfile",
15
+ "README.markdown",
16
+ "Rakefile",
17
+ "sqs_web.gemspec"
18
+ ] + %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(lib|spec|bin)} }
19
+
20
+ gem.extra_rdoc_files = [
21
+ "README.markdown"
22
+ ]
23
+
24
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
25
+
26
+ gem.add_runtime_dependency "sinatra", [">= 1.4.4"]
27
+ gem.add_dependency 'aws-sdk', '~> 2.1.33'
28
+
29
+
30
+ gem.add_development_dependency "rspec"
31
+ gem.add_development_dependency "rails", ["~> 3.0"]
32
+ gem.add_development_dependency "capybara"
33
+ gem.add_development_dependency "codeclimate-test-reporter"
34
+ gem.add_development_dependency "byebug"
35
+ gem.add_development_dependency "capybara-webkit"
36
+ #gem.add_development_dependency "fake_sqs", ["~> 0.3.1"]
37
+ end