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.
Related
I'm working on getting my Rails app interacting with the AWS Comprehend service for text entity extraction. I'm using the aws-sdk-comprehend gem. I have successfully gotten my app working with the AWS Rekognition service for image analysis using the aws-sdk-rekognition gem.
I can't seem to get the AWS Comprehend authentication correct as all of my calls result in an Aws::Comprehend::Errors::InvalidRequestException.
I have all of the following ENV variables set:
AWSAccessKeyId
AWSSecretKey
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
My code looks something like this:
class MyApp::Aws::ComprehendService < MyApp::ServiceBase
def call
#credentials = Aws::Credentials.new(ENV['AWSAccessKeyId'], ENV['AWSSecretKey'])
#client = Aws::Comprehend::Client.new(region: "us-west-1", credentials: credentials)
#client.detect_entities({text: "this is a simply little blob of text", language_code: "en"})
end
end
This resulted in Aws::Comprehend::Errors::InvalidRequestException. So I also tried:
class MyApp::Aws::ComprehendService < MyApp::ServiceBase
def call
# use ENV credential format I've seen in examples...
#credentials = Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'])
#client = Aws::Comprehend::Client.new(region: "us-west-1", credentials: credentials)
#client.detect_entities({text: "this is a simply little blob of text", language_code: "en"})
end
end
I found an example that didn't use the #credential approach. The example claimed "The initialize method will load the credentials environment variables by itself". So I tried this:
class MyApp::Aws::ComprehendService < MyApp::ServiceBase
def call
# ignore setting the credentials
#client = Aws::Comprehend::Client.new(region: "us-west-1")
#client.detect_entities({text: "this is a simply little blob of text", language_code: "en"}).
end
end
This also resulted in Aws::Comprehend::Errors::InvalidRequestException.
Can you see anything I'm doing wrong? Has anyone had success in using this gem to interact with the Comprehend API?
Per the Documentation for Aws::Comprehend::Client#detect_entities
If the system detects a document-level error in your input document, the API returns an InvalidRequestException error response. For details about this exception, see Errors in semi-structured documents in the Comprehend Developer Guide.
So it appears your usage is not necessarily the issue but rather the input documents themselves.
The response however should include what the actual issue is according to the Developer Guide:
Document-level errors
If the ClassifyDocument or DetectEntities API operation detects a document-level error in your input document, the API returns an InvalidRequestException error response.
In the error response, the Reason field contains the value INVALID_DOCUMENT.
The Detail field contains one of the following values:
DOCUMENT_SIZE_EXCEEDED – Document size is too large. Check the size of your file and resubmit the request.
UNSUPPORTED_DOC_TYPE – Document type is not supported. Check the file type and resubmit the request.
PAGE_LIMIT_EXCEEDED – Too many pages in the document. Check the number of pages in your file and resubmit the request.
TEXTRACT_ACCESS_DENIED_EXCEPTION – Access denied to Amazon Textract. Verify that your account has permission to use the Amazon Textract DetectDocumentText and AnalyzeDocument API operations and resubmit the request.
The Aws::Comprehend::Errors::InvalidRequestException object is documented so it appears you could potentially figure out what is wrong via
def call
# use ENV credential format I've seen in examples...
#credentials = Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'])
#client = Aws::Comprehend::Client.new(region: "us-west-1", credentials: credentials)
begin
#client.detect_entities({text: "this is a simply little blob of text", language_code: "en"})
rescue Aws::Comprehend::Errors::InvalidRequestException => e
# interrogate the error object here e.g.
puts {reason: e.reason, detail: e.detail}
end
end
Feb 8, 2018
My Ruby on Rails application has been successfully using ActiveMerchant::Billing::AuthorizeNetCimGateway with the payment type :credit_card for creating a customer profile with an embedded payment profile.
I'm now in the process of migrating to Authorize.Net's Accept.js which accepts credit card info directly sent from their hosted payment form and returns a payment nounce of type COMMON.ACCEPT.INAPP.PAYMENT that can be used for one time to create a payment transaction, customer profile, etc.
I constructed a payment_profile hash with :opaque_data in place of :credit_card. For example:
> payment_profile
=> {:payment=>
{:opaque_data=>
{:data_descriptor=>"COMMON.ACCEPT.INAPP.PAYMENT",
:data_value=> "eyJjb2RlIjoiNTBfMl8wNjAwMDUzNjBDMzAwOUQ3OEUzOUQ1MDk4QTYxMjFGNzlCQ0Y3RDRGQUE4NTNCMEU3MkYyMUJBNTI3NUE0NjQ2Q0ZFQTVFNzMxMDI2Qjg5ODJGNjBFRUE2RDZFMTZCMUY5NzQ4NUJFIiwidG9rZW4iOiI5NTE4MDc3Njg5NDA4MTAwOTAzNTAyIiwidiI6IjEuMSJ9"}},
:bill_to=>{:first_name=>"Firstname", :last_name=>"Lastname", :address=>nil, :city=>nil, :state=>nil, :zip=>nil, :country=>nil, :phone_number=>"(012) 234-5678"}}
I then tried to create a customer profile with an existing code similar to the following:
response = #gateway.create_customer_profile profile: {
email: client.email,
description: client.name,
merchant_customer_id: client.id,
payment_profiles: payment_profile
}
However, I received a response which had a result_code of Error and complained about "incomplete content" for element payment as follows:
> response
=> #<ActiveMerchant::Billing::Response:0x007f9827d14900
#authorization=nil,
#avs_result={"code"=>nil, "message"=>nil, "street_match"=>nil, "postal_match"=>nil},
#cvv_result={"code"=>nil, "message"=>nil},
#emv_authorization=nil,
#error_code="E00003",
#fraud_review=nil,
#message=
"The element 'payment' in namespace 'AnetApi/xml/v1/schema/AnetApiSchema.xsd' has incomplete content. List of possible elements expected: 'creditCard, bankAccount, trackData, encryptedTrackData, payPal, opaqueData, emv' in namespace 'AnetApi/xml/v1/schema/AnetApiSchema.xsd'.",
#params=
{"messages"=>
{"result_code"=>"Error",
"message"=>
{"code"=>"E00003",
"text"=>
"The element 'payment' in namespace 'AnetApi/xml/v1/schema/AnetApiSchema.xsd' has incomplete content. List of possible elements expected: 'creditCard, bankAccount, trackData, encryptedTrackData, payPal, opaqueData, emv' in namespace 'AnetApi/xml/v1/schema/AnetApiSchema.xsd'."}}},
#success=false,
#test=true>
I have a few questions in my mind:
Does ActiveMerchant::Billing::AuthorizeNetCimGateway even support Accept.js' :opaque_data in place of :credit_card?
If ActiveMerchant::Billing::AuthorizeNetCimGateway does support :opaque_data, what's may be wrong with the above payment_profile and what other content that I'd need to provide for payment element?
I'd appreciate any help in resolving this issue.
While this question is over 4 years old, and I'm assuming you have either found a solution or abandoned your effort, I ran into this same issue recently, and thought it would be helpful to add my findings in case someone else runs into this.
the AuthorizeNetCimGateway does not currently support Accept.js' opaqueData. In looking at the sourcecode for the active_merchant gem, specifically in /lib/active_merchant/billing/gateways/authorize_net_cim.rb, there is ultimately a method add_payment_profile that gets called. In that method, specifically on lines 759-761, you can see that the options are either a credit_card, bank_account, or drivers_license. A tokenized payment is not currently supported.
That being said, there is an open PR#2422 that adds support for this. At the time of writing this, it appears to be failing some rubocop specs, but hopefully it can get deployed in the near future!
I have AJAX calls initiated by Rails UJS that I would like to test. specifically, I have used Rails UJS ajax events to provide for cases of errors.
I would like to test them but I don't know how to tell rspec/capybara to "stub" and assume the error code
$("button").
on('ajax:error',function(event,xhr, status, error){
if(status == "timeout") {
var msg;
msg = Messenger().post({
message: "This is taking too long"
});
} else {
var msg;
msg = Messenger().post({
message: "it seems there is a bug. Please try again."
});
};
});
I would like to do something like the following:
describe "test returning error message", js: true do
it "should be successful" do
visit deal_page_path(deal)
first('a.button').click
stub(:xhr.status) = "timeout"
expect(page).to have_content('This is taking too long')
end
end
How to do this?
Note: the ajax requests are internal they don't go to third party API or services (such as facebook for ex).
When testing with Capybara (JS enabled drivers) it has no access to the request or response except through the changes it creates in the browser. You could build a test mode into your relevant controllers that could be turned on and off to allow it to output the errors you want, but the cleanest way to do this is probably to use a programmable proxy like puffing-billy which will allow you to selectively return whatever you'd like for any given request from the browser. One thing to realize is that this isn't testing that app correctly returns errors, it's just testing that your front-end handles errors the way you expect.
I'm looking at the testing docs for Savon here and i don't understand what's going on. I'm fairly new to testing with mocks and stubbing and maybe that's the issue. Here is the example:
require "spec_helper"
# require the helper module
require "savon/mock/spec_helper"
describe AuthenticationService do
# include the helper module
include Savon::SpecHelper
# set Savon in and out of mock mode
before(:all) { savon.mock! }
after(:all) { savon.unmock! }
describe "#authenticate" do
it "authenticates the user with the service" do
message = { username: "luke", password: "secret" }
fixture = File.read("spec/fixtures/authentication_service/authenticate.xml")
# set up an expectation
savon.expects(:authenticate).with(message: message).returns(fixture)
# call the service
service = AuthenticationService.new
response = service.authenticate(message)
expect(response).to be_successful
end
end
end
I understand that we set up an expectation with the fixture i.e. what the response should be.
We then call the service and get a response. My questions are:
1. Is a real call being made?
2. Is this response a real response??
3. Can someone try to explain this overall for me please?
Cheers
No remote request would be made. Since you have mocked authenticate, the response will be short-circuited to your designated value. However, some other preliminary requests might be expected to happen first, like a GET for the WSDL.
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.