How to change response.message in rails - ruby-on-rails

Hello I am having given code
def create_profile(payment)
return unless payment.source.gateway_customer_profile_id.nil?
options = {
email: payment.order.email,
login: preferred_secret_key,
}.merge! address_for(payment)
source = update_source!(payment.source)
if source.number.blank? && source.gateway_payment_profile_id.present?
creditcard = source.gateway_payment_profile_id
else
creditcard = source
end
response = provider.store(creditcard, options)
if response.success?
cc_type=payment.source.cc_type
response_cc_type = response.params['sources']['data'].first['brand']
cc_type = CARD_TYPE_MAPPING[response_cc_type] if CARD_TYPE_MAPPING.include?(response_cc_type)
payment.source.update_attributes!({
cc_type: cc_type, # side-effect of update_source!
gateway_customer_profile_id: response.params['id'],
gateway_payment_profile_id: response.params['default_source'] || response.params['default_card']
})
else
payment.send(:gateway_error, response.message)
end
end
I need to change message in response.message for that i had tried using response = [ { message: "fraud card"} ].to_json but it gives error `
undefined method `message' for "[{"message":"fraud card"}]":String
I had also tried response.message = 'fraud error', still it gives error. Response I receive is :
params:
error:
message: Your card was declined.
type: card_error
code: card_declined
decline_code: fraudulent
charge: ch_1AgncyJEfCzWOpKDdoxn1x1R
message: Your card was declined.
success: false
test: false
authorization: ch_1AgncyJEfCzWOpKDdoxn1x1R
fraud_review:
error_code: card_declined
emv_authorization:
avs_result:
code:
message:
street_match:
postal_match:
cvv_result:
code:
message:
Now my requirement is to check if decline_code is fraudulent than my message should be fraud error. please let me know how to change this.

Based on your comment, you're using Spree Gateway. By passing a string instead of the proper response object, your solution circumvents Spree's default implementation which logs error details for the response.
What I'd do instead is adapt the gateway_error method to your needs by following Spree's suggested approach for logic customization:
# app/models/spree/payment_decorator.rb
Spree::Payment.class_eval do
private
def gateway_error(error)
if error.is_a? ActiveMerchant::Billing::Response
# replace this with your actual implementation, e.g. based on response.params['error']['code']
text = 'fraud message'
elsif error.is_a? ActiveMerchant::ConnectionError
text = Spree.t(:unable_to_connect_to_gateway)
else
text = error.to_s
end
logger.error(Spree.t(:gateway_error))
logger.error(" #{error.to_yaml}")
raise Core::GatewayError.new(text)
end
end
It's not the cleanest implementation since it does copy & paste for existing code. But that's just how Spree is (I've implemented and contributed to multiple Spree shops and it's always a bit painful when customizing logic, especially private logic).
Hope that helps.

Related

I'm having trouble understanding how to use webmock with a test for fetching a message from an api

I'm currently trying to write a test for this method:
#fetch message from api
def message_instance
project_id = ENV['SIGNALWIRE_PROJECT_KEY']
project_tkn = ENV['SIGNALWIRE_TOKEN']
host_url = ENV['SIGNALWIRE_HOST']
#client = Signalwire::REST::Client.new project_id, project_tkn, signalwire_space_url: host_url
message = #client.messages(sid).fetch
end
and am using FactoryBot to simulate a received message
#message = build :message, status: :received
But I can't get my head around how to test every line of the method. Hoping someone can break down how I can properly stub a request for this?
Edit: I so far I've tried this:
describe 'message_instance' do
it 'returns message instance' do
allow(ENV).to receive(:[]).with('SIGNALWIRE_PROJECT_KEY').and_return('AC123')
#message = build :message, status: 'received', sid: '123456789'
#message.message_instance.should eq #client
end
end
which returns this error:
"BUNDLER_ORIG_RUBYLIB"=>"BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL", "BUNDLER_ORIG_RUBYOPT"=>"BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL", "BUNDLE_GEMFILE"=>"/vagrant/oyetext-backend/Gemfile", "BUNDLE_BIN_PATH"=>"/usr/share/rvm/rubies/ruby-2.7.2/lib/ruby/gems/2.7.0/gems/bundler-2.1.4/libexec/bundle", "BUNDLER_VERSION"=>"2.1.4", "RUBYOPT"=>"-r/usr/share/rvm/rubies/ruby-2.7.2/lib/ruby/2.7.0/bundler/setup", "RUBYLIB"=>"", "RAILS_ENV"=>"test", "SIGNALWIRE_PROJECT_KEY"=>"test", "SIGNALWIRE_TOKEN"=>"test", "SIGNALWIRE_NUMBER"=>"+19999999999"} received :[] with unexpected arguments
expected: ("SIGNALWIRE_PROJECT_KEY")
got: ("SIGNALWIRE_TOKEN")
Please stub a default value first if message might be received with other args as well.
I don't really think stubbing ENV is a good idea, as you could see that is used for way more stuff than your code logic, like bundler or ruby itself.
Instead of calling allow(ENV).to..., I'd try just with:
ENV['SIGNALWIRE_PROJECT_KEY'] = 'AC123'

How to DRY up Webhook handlers in Rails

I've been developing Stripe Webhook handler to create/update records depending the values.
It's not really hard, if it's a simple like this below;
StripeEvent.configure do |events|
events.subscribe 'charge.succeeded' do |event|
charge = event.data.object
StripeMailer.receipt(charge).deliver
StripeMailer.admin_charge_succeeded(charge).deliver
end
end
However If I need to store the data conditionally, it could be little messier.
In here I extracted the each Webhook handler and defined something like stripe_handlers/blahblah_handler.rb.
class InvoicePaymentFailed
def call(event)
invoice_obj = event.data.object
charge_obj = retrieve_charge_obj_of(invoice_obj)
invoice = Invoice.find_by(stripe_invoice_id: charge_obj[:invoice])
# common execution for subscription
invoice.account.subscription.renew_billing_period(start_at: invoice_obj[:period_start], end_at: invoice_obj[:period_end])
case invoice.state
when 'pending'
invoice.fail!(:processing,
amount_due: invoice[:amount_due],
error: {
code: charge_obj[:failure_code],
message: charge_obj[:failure_message]
})
when 'past_due'
invoice.failed_final_attempt!
end
invoice.next_attempt_at = Utils.unix_time_to_utc(invoice_obj[:next_payment_attempt].to_i)
invoice.attempt_count = invoice_obj[:attempt_count].to_i
invoice.save
end
private
def retrieve_charge_obj_of(invoice)
charge_obj = Stripe::Charge.retrieve(id: invoice.charge)
return charge_obj
rescue Stripe::InvalidRequestError, Stripe::AuthenticationError, Stripe::APIConnectionError, Stripe::StripeError => e
logger.error e
logger.error e.backtrace.join("\n")
end
end
end
I just wonder how I can DRY up this Webhook handler.
Is there some best practice to approach this or any ideas?
I suggest re-raising the exception in retrieve_charge_obj_of, since you'll just get a nil reference exception later on, which is misleading. (As is, you might as well let the exception bubble up, and let a dedicated error handling system rescue, log, and return a meaningful 500 error.)
a. If you don't want to return a 500, then you have a bug b/c retrieve_charge_obj_of will return nil after the exception is rescued. And if charge_obj is nil, then this service will raise a NPE, resulting in a 500.
if invoice_obj[:next_payment_attempt] can be !present? (blank?), then what is Utils.unix_time_to_utc(invoice_obj[:next_payment_attempt].to_i) supposed to mean?
a. If it was nil, false, or '', #to_i returns 0 -- is that intended? ([]/{} is also blank? but would raise)
Conceptually, this handler needs to issue a state transition on an Invoice, so a chunk of this logic can go in the model instead:
class Invoice < ApplicationRecord
# this method is "internal" to your application, so incoming params should be already "clean"
def mark_payment_failed!(err_code, err_msg, attempt_count, next_payment_at)
transaction do # payment processing usually needs to be transactional
case self.state
when 'pending'
err = { code: err_code, message: err_msg }
self.fail!(:processing, amount_due: self.amount_due, error: err)
when 'past_due'
self.failed_final_attempt!
else
ex_msg = "some useful data #{state} #{err_code}"
raise InvalidStateTransition, ex_msg
end
self.next_attempt_at = next_payment_at
self.attempt_count = attempt_count
self.save
end
end
class InvalidStateTransition < StandardError; end
end
Note: I recommend a formal state machine implementation (e.g. state_machine) before states & transitions get out of hand.
Data extraction, validation, and conversion should happen in the handler (that's what "handlers" are for), and they should happen before flowing deeper in your application. Errors are best caught early and execution stopped early, before any action has been taken.
There are still some other edge cases that I see that aren't really handled.

RSpec 3 - raising an error on purpose to test it is rescued

Consider a simple method -
def my_method(users)
eligible_users = []
users.each do |u|
# Go to the next user unless they are eligible
next unless is_eligible?(u)
begin
update_user(u)
eligible_users << u
rescue
puts "Error occured"
# Prints some other stuff about error
next
end
end
end
A key feature of this method is that it loops through users but continues to the next user even if a given user throws an error.
If I were writing spec tests for this, I'd love to pass an array of 3 users and purposely have it error out on the first user. I can then check that the 2nd and 3rd were still correctly processed.
How would I go about raising an error on purpose for only one of the result sets?
I was thinking I could stub is_eligible? and return an error for one of the result and true for the remainder -
allow_any_instance_of(MyClass).to receive(:is_eligible?).and_return(
raise StandardError.new,
true,
true
)
As expected, that doesn't work. Any other approaches?
Thanks!
I can't exactly answer the question, but rather than doing a begin resuce thing,you can follow this approach,
Make the update_user return true or false.
Keep an array of users that falied to update.
return an object like
response: { status: "failure", message: "falied to update #{pluralize(failed_users_array.count, 'user')}", failures: failed_users_array.join(", ") } for failures and
response: { status: "success", message: "#{pluralize(users.count, 'user')} updated successfully" } for all success.
now you can easily test,
have two cases, one where you can test failures and when you can test all success.
Just stub the response object.
For raising errors, you have to do .and_raise("some tezt or StandardError.new") , thats in the docs.

Ruby on Rails - Checking against HTTP errors in controller

Just today I've found my fbgraph implementation has started returning a 400 Bad Request error which is causing an internal server error.
The controller looks like:
def fb
fbclient = FBGraph::Client.new(:client_id => 'ID', :secret_id => 'SECRET')
#fbname = fbclient.selection.user('129220333799040').feed.info!['data'][0].from.name
#fbmessage = fbclient.selection.user('129220333799040').feed.info!['data'][0].message
end
How can I check before calling #fbname in my view that I've received a 200 status?
Thanks.
Update: following Devin M's suggestion, I've switched the above action to
def fb
fbclient = FBGraph::Client.new(:client_id => 'ID', :secret_id => 'SECRET')
begin
#fbname = fbclient.selection.user('129220333799040').feed.info!['data'][0].from.name
#fbmessage = fbclient.selection.user('129220333799040').feed.info!['data'][0].message
rescue
#fbname = "Facebook Account"
#fbmessage = "Facebook's API is a nightmare"
end
end
I think that you should write some tests for this, Its hard to work with Facebooks nightmare of an API.
Although if you wanted to catch this error try using that way you can catch the specific error and take some action on it in the rescue portion.
begin
rescue
end
If you want me to take a look at the docs and see what you should catch let me know.

Problems with MailChimp API in Ruby Error Code: -90

I am using the following code in my MailChimp Controller to submit simple newsletter data. When It is sent I receive the following error as a "Method is not exported by this server -90" I have attached my controller code below. I am using this controller for a simple newsletter signup form. (Name, Email)
class MailchimpController < ApplicationController
require "net/http"
require "uri"
def subscribe
if request.post?
mailchimp = {}
mailchimp['apikey'] = 'f72328d1de9cc76092casdfsd425e467b6641-us2'
mailchimp['id'] = '8037342dd1874'
mailchimp['email_address'] = "email#gmail.com"
mailchimp['merge_vars[FNAME]'] = "FirstName"
mailchimp['output'] = 'json'
uri = URI.parse("http://us2.api.mailchimp.com/1.3/?method=listSubscribe")
response = Net::HTTP.post_form(uri, mailchimp)
mailchimp = ActiveSupport::JSON.decode(response.body)
if mailchimp['error']
render :text => mailchimp['error'] + "code:" + mailchimp['code'].to_s
elsif mailchimp == 'true'
render :text => 'ok'
else
render :text => 'error'
end
end
end
end
I highly recommend the Hominid gem: https://github.com/tatemae-consultancy/hominid
The problem is that Net::HTTP.post_form is not passing the "method" GET parameter. Not being a big ruby user, I'm not certain what the actual proper way to do that with Net::HTTP is, but this works:
require "net/http"
data="apikey=blahblahblah"
response = nil
Net::HTTP.start('us2.api.mailchimp.com', 80) {|http|
response = http.post('/1.3/?method=lists', data)
}
p response.body
That's the lists() method (for simplicity) and you'd have to build up (and urlencode your values!) your the full POST params rather than simply providing the hash.
Did you take a look at the many gems already available for ruby?
http://apidocs.mailchimp.com/downloads/#ruby
The bigger problem, and main reason I'm replying to this, is that your API Key is not obfuscated nearly well enough. Granted I'm used to working with them, but I was able to guess it very quickly. I would suggest immediately going and disabling that key in your account and then editing the post to actually have completely bogus data rather than anything close to the correct key. The list id on the other hand, doesn't matter at all.
You'll be able to use your hash if you convert it to json before passing it to Net::HTTP. The combined code would look something like:
mailchimp = {}
mailchimp['apikey'] = 'APIKEYAPIKEYAPIKEYAPIKEY'
mailchimp['id'] = '8037342dd1874'
mailchimp['email_address'] = "email#gmail.com"
mailchimp['merge_vars[FNAME]'] = "FirstName"
mailchimp['output'] = 'json'
response = nil
Net::HTTP.start('us2.api.mailchimp.com', 80) {|http|
response = http.post('/1.3/?method=listSubscribe', mailchimp.to_json)
}

Resources