How would I use rspec to test a method who's job is to post to a webhook? - ruby-on-rails

I'm using rspec to test my application and I'm having a hard time figuring out how to test this. The Slack::Notifier's job is to send a post request to a webhook. Once I call this method in Rspec, I don't know how to see the response. Also, is it possible to match the format of this text to an expected text somewhere? My method is below. Thanks.
def notify
offset = 14400 #UTC to EST
notifier = Slack::Notifier.new Rails.application.secrets.slack_organization_name, Rails.application.secrets.slack_token, channel: "##{Rails.application.secrets.slack_channel}", username: Rails.application.secrets.slack_user_name
notifier.ping(":white_check_mark: *USAGE SUMMARY for #{(Time.now - offset).to_formatted_s(:long) }*")
count = 0
current_time = Time.now.to_i
live_response.each do |r|
if r["properties"]["time"] > ((current_time - offset) - 60) #&& r["properties"]["$initial_referring_domain"] == "capture.com"
notifier.ping("
*Name:* #{r["properties"]["$name"]}
*Event:* #{r["event"]}
*Keywords:* #{r["properties"]["keywords"]}
*Organization:* #{r["properties"]["organizationName"]}
*Email:* #{r["properties"]["$email"]}
*Time:* #{Time.at(r["properties"]["time"] + offset).utc.to_datetime.in_time_zone("Eastern Time (US & Canada)").to_formatted_s(:long_ordinal)}
*More Data:* #{ANALYTICS_URL}#{r["properties"]["distinct_id"]}
__________________________________________________
")
count +=1
end
end
notifier.ping("*There were #{count} events in this report.*")
end

Testing network communications (like API calls) is a tricky thing. Personally I would rely on programming by contract and testing in isolation - i.e. assume the external service is working fine and it responds positively for valid request.
Then you test your client code by checking that you are actually sending a valid request. For this stub the method where control exits your code into a library/system code. For example if you are making a HTTP GET request using a gem like HTTParty, then stub HTTParty.get i.e. HTTParty.stub(:get) and in that stub verify that correct parameters were sent.
On the other side of the spectrum you should also simulated both positive and negative responses from the web service and make sure your client code handles it in expected manner.
If you are making a real then you are introducing a lot of dependencies on your test : a test setup of external service, risk of network issues (timeout, n/w breakdown, etc) problems with external service and may be more.
If you yourself are writing that webservice too then test that one also in isolation, i.e by simulating valid and invalid inputs and making sure they are handled properly. This part is pretty much your controller specs or request specs.
Once again, this is my opinion. Suggestions to do this in a better way and constructive criticism on the shortcomings of this approach are definitely welcome.

Related

Rails - multiple theads to avoid the slack 3 second API response rule

I am working with the slack API. My script does a bunch of external processing and in some cases it can take around 3-6 seconds. What is happening is the Slack API expects a 200 response within 3 seconds and because my function is not finished within 3 seconds, it retries again and then it ends up posting the same automated responses 2-3 times.
I confirmed this by commenting out all the functions and I had no issue, it posted the responses to slack fine. I then added sleep 10 and it done the same responses 3 times so the ohly thing different was it took longer.
From what I read, I need to have threaded responses. I then need to first respond to the slack API in thread 1 and then go about processing my functions.
Here is what I tried:
def events
Thread.new do
json = {
"text": "Here is your 200 response immediately slack",
}
render(json: json)
end
puts "--------------------------------Json response started----------------------"
sleep 30
puts "--------------------------------Json response completed----------------------"
puts "this is a successful response"
end
When I tested it the same issue happened so I tried using an online API tester and it hits the page, waits 30 seconds and then returns the 200 response but I need it to respond immediately with the 200, THEN process the rest otherwise I will get duplicates.
Am I using threads properly or is there another way to get around this Slack API 3 second response limit? I am new to both rails and slack API so a bit lost here.
Appreciate the eyes :)
I would recommend using ActionJob to run the code in the background if you don't need to use the result of the code in the response. First, create an ActiveJob job by running:
bin/rails generate job do_stuff
And then open up the file created in app/jobs/do_stuff_job.rb and edit the #perform function to include your code (so the puts statements and sleep 30 in your example). Finally, from the controller action you can call DoStuff.perform_later and your job will run in the background! Your final controller action will look something like this:
def events
DoStuff.perform_later # this schedules DoStuff to be done later, in
# the background, so it will return immediately
# and continue to the next line.
json = {
"text": "Here is your 200 response immediately slack",
}
render(json: json)
end
As an aside, I'd highly recommend never using Thread.new in rails. It can create some really confusing behavior especially in test scripts for a number of reasons, but usually because of how it interacts with open connections and specifically ActiveRecord.

Preventing rapid-fire login attempts with Rack::Attack

We have been reading the Definitive guide to form based website authentication with the intention of preventing rapid-fire login attempts.
One example of this could be:
1 failed attempt = no delay
2 failed attempts = 2 sec delay
3 failed attempts = 4 sec delay
etc
Other methods appear in the guide, but they all require a storage capable of recording previous failed attempts.
Blocklisting is discussed in one of the posts in this issue (appears under the old name blacklisting that was changed in the documentation to blocklisting) as a possible solution.
As per Rack::Attack specifically, one naive example of implementation could be:
Where the login fails:
StorageMechanism.increment("bad-login/#{req.ip")
In the rack-attack.rb:
Rack::Attack.blacklist('bad-logins') { |req|
StorageMechanism.get("bad-login/#{req.ip}")
}
There are two parts here, returning the response if it is blocklisted and check if a previous failed attempt happened (StorageMechanism).
The first part, returning the response, can be handled automatically by the gem. However, I don't see so clear the second part, at least with the de-facto choice for cache backend for the gem and Rails world, Redis.
As far as I know, the expired keys in Redis are automatically removed. That would make impossible to access the information (even if expired), set a new value for the counter and increment accordingly the timeout for the refractory period.
Is there any way to achieve this with Redis and Rack::Attack?
I was thinking that maybe the 'StorageMechanism' has to remain absolutely agnostic in this case and know nothing about Rack::Attack and its storage choices.
Sorry for the delay in getting back to you; it took me a while to dig out my old code relating to this.
As discussed in the comments above, here is a solution using a blacklist, with a findtime
# config/initilizers/rack-attack.rb
class Rack::Attack
(1..6).each do |level|
blocklist("allow2ban login scrapers - level #{level}") do |req|
Allow2Ban.filter(
req.ip,
maxretry: (20 * level),
findtime: (8**level).seconds,
bantime: (8**level).seconds
) do
req.path == '/users/sign_in' && req.post?
end
end
end
end
You may wish to tweak those numbers as desired for your particular application; the figures above are only what I decided as 'sensible' for my particular application - they do not come from any official standard.
One issue with using the above that when developing/testing (e.g. your rspec test suite) the application, you can easily hit the above limits and inadvertently throttle yourself. This can be avoided by adding the following config to the initializer:
safelist('allow from localhost') do |req|
'127.0.0.1' == req.ip || '::1' == req.ip
end
The most common brute-force login attack is a brute-force password attack where an attacker simply tries a large number of emails and passwords to see if any credentials match.
You should mitigate this in the application by use of an account LOCK after a few failed login attempts. (For example, if using devise then there is a built-in Lockable module that you can make use of.)
However, this account-locking approach opens a new attack vector: An attacker can spam the system with login attempts, using valid emails and incorrect passwords, to continuously re-lock all accounts!
This configuration helps mitigate that attack vector, by exponentially limiting the number of sign-in attempts from a given IP.
I also added the following "catch-all" request throttle:
throttle('req/ip', limit: 300, period: 5.minutes, &:ip)
This is primarily to throttle malicious/poorly configured scrapers; to prevent them from hogging all of the app server's CPU.
Note: If you're serving assets through rack, those requests may be counted by rack-attack and this throttle may be activated too quickly. If so, enable the condition to exclude them from tracking.
I also wrote an integration test to ensure that my Rack::Attack configuration was doing its job. There were a few challenges in making this test work, so I'll let the code+comments speak for itself:
class Rack::AttackTest < ActionDispatch::IntegrationTest
setup do
# Prevent subtle timing issues (==> intermittant test failures)
# when the HTTP requests span across multiple seconds
# by FREEZING TIME(!!) for the duration of the test
travel_to(Time.now)
#removed_safelist = Rack::Attack.safelists.delete('allow from localhost')
# Clear the Rack::Attack cache, to prevent test failure when
# running multiple times in quick succession.
#
# First, un-ban localhost, in case it is already banned after a previous test:
(1..6).each do |level|
Rack::Attack::Allow2Ban.reset('127.0.0.1', findtime: (8**level).seconds)
end
# Then, clear the 300-request rate limiter cache:
Rack::Attack.cache.delete("#{Time.now.to_i / 5.minutes}:req/ip:127.0.0.1")
end
teardown do
travel_back # Un-freeze time
Rack::Attack.safelists['allow from localhost'] = #removed_safelist
end
test 'should block access on 20th successive /users/sign_in attempt' do
19.times do |i|
post user_session_url
assert_response :success, "was not even allowed to TRY to login on attempt number #{i + 1}"
end
# For DOS protection: Don't even let the user TRY to login; they're going way too fast.
# Rack::Attack returns 403 for blocklists by default, but this can be reconfigured:
# https://github.com/kickstarter/rack-attack/blob/master/README.md#responses
post user_session_url
assert_response :forbidden, 'login access should be blocked upon 20 successive attempts'
end
end

object_double for Mandril::API

When using the mandril-api the Rails Action Mailer is bypassed, so it is not possible to do something like this
it 'sends an email' do
expect { subject.send_instructions }
.to change { ActionMailer::Base.deliveries.count }.by(1)
end
I am trying to use an object_double to test my mailers. What I am trying to test is exactly what parameters are sent to the API (via an options hash).
So far, I have the Mandrill code here
MANDRILL.messages.send_template( options[:template], [], message) unless Rails.env.staging?
Where MANDRILL is just the connection to the API, as detailed below.
describe 'verify content' do
it 'uses the correct template' do
api = object_double(Mandrill::API.new(ENV['MANDRILL_KEY']).messages)
allow(api).to receive(:send_template)
PostNotificationMailer.format_options(participant, post)
expect(api).to have_received(:send_template)
#expect(options[:template]).to eq('crowdai_standard_template')
end
end
I'd like to be able to check all the params passed to the Mandrill API here. I can mock the messages method but not the messages.send_template
1) PostNotificationMailer verify content uses the correct template
Failure/Error: expect(api).to have_received(:send_template)
(ObjectDouble(#<Mandrill::Messages:0x007f8debd4f348 #master=#<Mandrill::API:0x007f8debd4faf0 #host="https://mandrillapp.com", #path="/api/1.0/", #session=#<Excon::Connection:7f8debd4f758 #data={:chunk_size=>1048576, :ciphers=>"HIGH:!SSLv2:!aNULL:!eNULL:!3DES", :connect_timeout=>60, :debug_request=>false, :debug_response=>false, :headers=>{"User-Agent"=>"excon/0.51.0"}, :idempotent=>false, :instrumentor_name=>"excon", :middlewares=>[Excon::Middleware::Hijack, Excon::Middleware::ResponseParser, Excon::Middleware::Expects, Excon::Middleware::Idempotent, Excon::Middleware::Instrumentor, Excon::Middleware::Mock], :mock=>false, :nonblock=>true, :omit_default_port=>false, :persistent=>false, :read_timeout=>60, :retry_limit=>4, :ssl_verify_peer=>true, :ssl_uri_schemes=>["https"], :stubs=>:global, :tcp_nodelay=>false, :thread_safe_sockets=>true, :uri_parser=>URI, :versions=>"excon/0.51.0 (x86_64-darwin15) ruby/2.3.1", :write_timeout=>60, :host=>"mandrillapp.com", :hostname=>"mandrillapp.com", :path=>"", :port=>443, :query=>nil, :scheme=>"https"} #socket_key="https://mandrillapp.com:443">, #debug=false, #apikey="redacted">>) (anonymous)).send_template(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
# ./spec/mailers/post_notification_mailer_spec.rb:14:in `block (3 levels) in <top (required)>'
** EDIT **
There is a gem MandrillMailer which solves the problem of testing against the Mandril API, but it's build is broken and it also seems to rebuild the API internally.
How do I test mandrill api with rspec
I couldn't find any tutorials or clear examples on how to use object_double.
Have you thought about using the VCR gem ( https://github.com/vcr/vcr ) to record the response from the API call to mandrill into a fixture? Once the request is recorded, you can assert the values on the response to verify the expected data was passed.
As best as I can tell from your code, PostNotificationMailer.format_options(participant, post) has no way to know that its code is supposed to be sending the send_template method to your double instead of the predefined MANDRILL.messages object. If you call Mandrill::API.new(ENV['MANDRILL_KEY']) in your test, that returns a completely different object from MANDRILL even if you defined MANDRILL with the exact same code. So when the mailer sends the method to MANDRILL.messages, your double is oblivious.
Unfortunately, even if your test was rewritten to make the double based on MANDRILL.messages, it still wouldn't be the same object as what's in your mailer, because the mailer is calling the real MANDRILL.messages and not your double. The way I understand it, for most doubles you still have to use dependency injection. That is, your mailer would have to be set up so that you could set a parameter that would be "the object that does the mailing," something like (I'm making this up) PostNotificationMailer.set_api(some_object). In production, it would be PostNotificationMailer.set_api(MANDRILL), while in your test it would be PostNotificationMailer.set_api(api). Possibly that's more trouble than it's worth.
This seems to be confirmed by the object_double documentation, where the test includes:
user = object_double(User.new, :save => true)
expect(save_user(user)).to eq("saved!")
As you can see, the user object is passed as a parameter into the method that we're trying to test so that methods are called on the double and not some other object.
RSpec does seem to have the interesting ability to use object doubles on constants, so that you don't have to use dependency injection. However, based on the relevant documentation, it looks like you have to pass the object name as a string (not the actual object reference) and then you have to call as_stubbed_const on the double:
logger = object_double("MyApp::LOGGER", :info => nil).as_stubbed_const
Email.send_to('hello#foo.com')
expect(logger).to have_received(:info).with("Sent to hello#foo.com")
So maybe if your application defined a constant for the API's messages object, and then passed in its name as a string and called as_stubbed_const, it would work. I haven't tried using RSpec's doubles like this, so I can't say for sure.

KairosDB Java client checks health

I would like to use KairosDB Java client to check KairosDB health but it seems there is too few guides. Anyone knows please help me?
I have commented the question to get more details about what you want to do.
However one interesting metric is the HTTP request time in kairosDB (kairosdb.http.request_time). By polling this metric you will:
- Make sure metrics are recorded
- Make sure http requests are received, processed and answered in reasonable time (although long queries will report longer time than others)
To do so you can follow the example on https://github.com/kairosdb/kairosdb-client, e.g. by doing this kind of query every five minutes:
QueryBuilder builder = QueryBuilder.getInstance();
builder.setStart(10, TimeUnit.MINUTES)
.setEnd(0, TimeUnit.MINUTES)
.addMetric("kairosdb.http.request_time")
.addGrouper(new TagGrouper("host"));
HttpClient client = new HttpClient("http://localhost:8080");
QueryResponse response = client.query(builder);
client.shutdown();
I hope this helps.
Regards,
Loic

Optimal way to structure polling external service (RoR)

I have a Rails application that has a Document with the flag available. The document is uploaded to an external server where it is not immediately available (takes time to propogate). What I'd like to do is poll the availability and update the model when available.
I'm looking for the most performant solution for this process (service does not offer callbacks):
Document is uploaded to app
app uploads to external server
app polls url (http://external.server.com/document.pdf) until available
app updates model Document.available = true
I'm stuck on 3. I'm already using sidekiq in my project. Is that an option, or should I use a completely different approach (cron job).
Documents will be uploaded all the time and so it seems relevant to first poll the database/redis to check for Documents which are not available.
See this answer: Making HTTP HEAD request with timeout in Ruby
Basically you set up a HEAD request for the known url and then asynchronously loop until you get a 200 back (with a 5 second delay between iterations, or whatever).
Do this from your controller after the document is uploaded:
Document.delay.poll_for_finished(#document.id)
And then in your document model:
def self.poll_for_finished(document_id)
document = Document.find(document_id)
# make sure the document exists and should be polled for
return unless document.continue_polling?
if document.remote_document_exists?
document.available = true
else
document.poll_attempts += 1 # assumes you care how many times you've checked, could be ignored.
Document.delay_for(5.seconds).poll_for_finished(document.id)
end
document.save
end
def continue_polling?
# this can be more or less sophisticated
return !document.available || document.poll_attempts < 5
end
def remote_document_exists?
Net::HTTP.start('http://external.server.com') do |http|
http.open_timeout = 2
http.read_timeout = 2
return "200" == http.head(document.path).code
end
end
This is still a blocking operation. Opening the Net::HTTP connection will block if the server you're trying to contact is slow or unresponsive. If you're worried about it use Typhoeus. See this answer for details: What is the preferred way of performing non blocking I/O in Ruby?

Resources