Rails test that method is called from the controller - ruby-on-rails

I have a controller function that calls a service and I want to test that the service is called with the right arguments.
def send_for_signature
client = Client.find(params[:client_id])
external_documents = params[:document_ids].map{|id| ExternalDocument.find(id)}
service = EsignGenieSendByTemplate.new(client: client, external_documents: external_documents, form_values: params[:form_values])
result = service.process
if result["result"] == "success"
head 200
else
render json: result["error_description"], status: :unprocessable_entity
end
end
How would I write my test to ensure that EsignGenieSendByTemplate.new(client: client, external_documents: external_documents, form_values: params[:form_values]) is called correctly?

I would start by adding a factory method to the service:
class EsignGenieSendByTemplate
# ...
def self.process(**kwargs)
new(**kwargs).process
end
end
This kind of code is boilerplate in almost any kind of service object and provides a better API between the service object and its consumers (like the controller).
This method should be covered by an example in your service spec.
describe '.process' do
let(:options) do
{ client: 'A', external_documents: 'B', form_values: 'C' }
end
it "forwards its arguments" do
expect(described_class).to recieve(:new).with(**options)
EsignGenieSendByTemplate.process(**options)
end
it "calls process on the instance" do
dbl = instance_double('EsignGenieSendByTemplate')
allow(described_class).to recieve(:new).and_return(dbl)
expect(dbl).to recieve(:process)
EsignGenieSendByTemplate.process(**options)
end
end
Your controller should just call the factory method instead of instanciating EsignGenieSendByTemplate:
def send_for_signature
client = Client.find(params[:client_id])
# Just pass an array to .find instead of looping - this create a single
# db query instead of n+1
external_documents = ExternalDocument.find(params[:document_ids])
result = EsignGenieSendByTemplate.process(
client: client,
external_documents: external_documents,
form_values: params[:form_values]
)
if result["result"] == "success"
head 200
else
render json: result["error_description"], status: :unprocessable_entity
end
end
This better API between the controller and service lets you set an expectation on the EsignGenieSendByTemplate class instead so you don't have to monkey around with expect_any_instance or stubbing the .new method.
it 'requests the signature' do
expect(EsignGenieSendByTemplate).to receive(:process).with(client: 'A', external_documents: 'B', form_values: 'C')
get :send_for_signature, params: { ... }
end

What you need is something called expecting messages.
I usually write something like this:
it 'requests the signature' do
expect(EsignGenieSendByTemplate).to receive(:new).with(client: 'A', external_documents: 'B', form_values: 'C')
get :send_for_signature, params: { ... }
expect(response.status).to have_http_status(:success)
end

Related

Rails Rspec - How to test if Service has been called in another Service

While writing tests, I stopped at trying to test Service in another Service. In such a situation, I should probably just check if Service has been called because it has already been tested elsewhere. I did a little research on the Internet and found something like have_received but I have no idea how to use it in my example.
check_service.rb
Class CheckService
def initialize(params)
#params = params
end
def self.call(params)
new(params).call
end
def call
CheckUser.call(params[:user_id])
end
end
check_service_spec.rb
...
describe 'call' do
let(:result) { CheckService.call(params) }
let(:params) { { user_id: "100" } }
let(:check_user) { instance_double(CheckUser) }
before do
allow(check_user).to receive(:call).and_return(true)
end
it do
result
expect(check_user).to have_received(:call)
end
end
...
I was trying something like this (it's simple example), but I get error:
(InstanceDouble(CheckUser) (anonymous)).call(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Is there any option to test situation I presented?
Short anwser
describe 'call' do
let(:result) { CheckService.call(params) }
let(:params) { { user_id: "100" } }
## let(:check_user) { instance_double(CheckUser) } delete this
before do
allow(CheckUser).to receive(:call).and_return(true)
end
it do
result
expect(CheckUser).to have_received(:call)
end
end
Alternative
I think a better way to test this is to use DI (Dependency Injection), so you pass CheckUser as a dependency to CheckService. I prefer to write the whole test inside the it block too!
class CheckService
def initialize(params, check_handler:)
#params = params
#check_handler = check_handler
end
def self.call(params, check_handler: CheckUser)
new(params, check_handler: check_handler).call
end
def call
#check_handler.call(#params[:user_id])
end
end
describe 'call' do
it 'check user with params' do
check_user = class_double(CheckUser)
allow(check_user).to receive(:call).and_return(true)
params = { user_id: "100" }
CheckService.call(params, check_handler: check_user)
expect(check_user).to have_received(:call)
end
end
A blog post to read more about -> https://blog.testdouble.com/posts/2018-05-17-do-we-need-dependency-injection-in-ruby/

Parse Json Request from CURL in Rails

I need to parse Json data from curl request.Need to split Mac,Parameter,datatype,value from below curl request and pass those data's as input for set method(For each Mac separately).Can anyone please guide how to split?
curl -k -s -H "Content-Type: application/json" -d '{"Data": {"Mac":"10.43.33.34","Parameter":"Device.wifi","datatype":"string","value":"5Ghz"},{"Mac":"15.23.43.48","Parameter":"Device.wifi","datatype":"string","value":"2.4GHZ"}}' http://test:3000/api/executions_api_set/
Set API
def show
client = SetClient.new
versionResponse = client.set_req(mac,parameter,datatype,value)
if versionResponse.code == "200"
value = JSON.parse(versionResponse.body)
render json: {Mac: mac,Response:value}, status: :ok
else
render json: {Mac: mac,Parameter: parameter,status: 'Failed',responsecode:versionResponse.code}, status: :ok
end
end
end
end
end
Updated Code
def create
value = ''
client = SetClient.new
params["Data"].each do |mac_attributes|
#mac_address, #Parameter, #dataType, #value = mac_attributes.values_at("Mac", "Parameter", "datatype", "value")
#versionResponse = client.set_req_api(#mac_address,#Parameter,#dataType,#value)
puts "versionResponse.status_code #{#versionResponse.code}"
end
if #versionResponse.code == "200"
value = JSON.parse(#versionResponse.body)
render json: {Mac_address: #mac_address,Response:value}, status: :ok
else
render json: {Mac_address: #mac_address,Parameter:#Parameter,status: 'Failed',responsecode:#versionResponse.code}, status: :ok
end
end
end
end
I'm not entirely sure what you're trying to do, but if you want the controller to call a method for each JSON object in the array, it would look something like:
def show
params["Data"].each do |hash_of_mac_attributes|
mac, parameter, datatype, value = hash_of_mac_attributes.values_at("Mac", "Parameter", "datatype", "value")
method_you_want_to_call(mac, parameter, datatype, value)
end
This is assuming that rails is correctly detecting and parsing the JSON body into the params hash for you. If it's not, that's a separate issue that can be addressed.
UPDATE
In order to return the result from each call to the external service we need to store each call in an array and render the array in the response. Something like the following:
def show
json_array = params["Data"].map do |hash_of_mac_attributes|
mac, parameter, datatype, value = hash_of_mac_attributes.values_at("Mac", "Parameter", "datatype", "value")
response = client.set_req_api(mac, parameter, datatype, value)
build_json_for_mac_lookup(response: response, mac: mac, parameter: parameter)
end
render json: json_array
end
def build_json_for_mac_lookup(arguments:, mac:, parameter:)
json_body = {Mac_address: mac}
if response.code == "200"
json_body.merge(Response: JSON.parse(response.body))
else
json_body.merge(Parameter: parameter, status: 'Failed', responsecode: response.code)
end
end

Rspec controller spec claims function ios not called

I am testing out one of my controllers and have attempted to stub a function call with no luck. Here is the function:
def fetch_typeform_response
hp = HealthProfile.find(params[:id])
form = TypeformService.new('x')
response = form.getResponse("query=#{ hp[:id] }")
if response['total_items'] != 1
if response[:response_id].present?
response = form.getResponse("included_response_ids=#{ hp[:response_id] }")
end
end
if response['total_items'] == 1
response = response['items'].first
health_profile = HealthProfile.map_typeform_response(response)
if health_profile.save
health_profile.reload
redirect_to health_profile_path(health_profile), notice: "Successfully updated the health profile response."
return
end
end
redirect_to health_profiles_path, notice: "We could not locate the health profile."
end
In my test, I stub out :getResponse and :map_typeform_response since they involve an outside API:
it "expects to fetch typeform response" do
new_hp = build(:health_profile)
new_hp_after_mapping = build(:health_profile)
allow_any_instance_of(TypeformService).to receive(:getResponse).and_return({ 'total_items': 1, 'items': [ new_hp ] }.as_json)
allow_any_instance_of(HealthProfile).to receive(:map_typeform_response).and_return(new_hp_after_mapping)
get :fetch_typeform_response, params: { id: #hp.id }
expect(response).to redirect_to(health_profile_path(#hp.id))
end
But I receive the error: HealthProfile does not implement #map_typeform_response.
If I remove the stub line, I see the error:
Failure/Error: p "Using health_profile_id: #{response['hidden']['id']}"
NoMethodError:
undefined method `[]' for nil:NilClass
Which is occurring inside the :map_typeform_response function (so clearly it is called!). Any idea why this might happen?
You are calling map_typeform_response method on class HealthProfile and not on instance of the class.
change
allow_any_instance_of(HealthProfile).to receive(:map_typeform_response).and_return(new_hp_after_mapping)
to
allow(HealthProfile).to receive(:map_typeform_response).and_return(new_hp_after_mapping)
That happens because rspec prevents you from mocking or stubbing a method that does not exist on a real object. Default is true since Rails 4.
RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
end
Some more recommendations
I'd also recommend to move building of new_hp and new_hp_after_mapping variables to let
let(:new_hp) { build(:health_profile) }
let(:new_hp_after_mapping) { build(:health_profile) }
move stubs to before
before do
allow_any_instance_of(TypeformService).to receive(:getResponse).and_return({ 'total_items': 1, 'items': [ new_hp ] }.as_json)
allow(HealthProfile).to receive(:map_typeform_response).and_return(new_hp_after_mapping)
end
so your test will look like
it "expects to fetch typeform response" do
# make sure variable #hp intialized in your test.
get :fetch_typeform_response, params: { id: #hp.id }
expect(response).to redirect_to(health_profile_path(#hp.id))
end

Trigger rails controller function - Paypal Website Standard IPN

I've got a Paypal IPN that comes into a PaymentNotificationsController in my app. However, some variables depend on the number of items in a cart, so i want to extract them before creating the PaymentNotification.
So far, i've got:
class PaymentNotificationsController < ApplicationController
protect_from_forgery except: [:create]
def create
PaymentNotification.create!(params: params,
item_number: params[:item_number], item_name: params[:item_name], quantity: params[:quantity]
render nothing: true
end
end
However, when the notification comes from PayPal, it comes in the form of item_name1, item_number1, quantity1, item_name2, item_number2, quantity2 and so on.
Even if its just one item, it would come as item_name1, item_number1, quantity1, option1 and so on.
I have this function to try and extract the variables, but i don't know how to trigger the function. I tried using a before_action at the top of the controller but it didn't work. Returned wrong number of arguments(0 for 1):
ITEM_PARAM_PREFIXES = ["item_name", "item_number", "quantity"]
def extract_ipn_items_params(params)
item_params = []
loop do
item_num_to_test = item_params.length + 1
item_num_suffix = item_num_to_test.to_s
possible_param_name = ITEM_PARAM_PREFIXES[0] + item_num_suffix
if params.include?(possible_param_name)
this_item_params = {}
ITEM_PARAM_PREFIXES.each do |prefix|
this_item_params[prefix] = params[prefix + item_num_suffix]
end
item_params.push this_item_params
else
return item_params
end
end
end
So i'm asking, how do i trigger the function to extract the variables and put them into params[:item_number], params[:item_name], params[:quantity] for each item in the cart so if there are two items, two separate PaymentNotifications would be created?
Note: Both methods are in the same PaymentNotificationsController.
Any help would be appreciated. Thanks in advance!
I assume your method extract_ipn_items_params already fetches the data you require, you can remove the params argument to the method, as the params is always available in the actions/methods of the controller.
ITEM_PARAM_PREFIXES = ["item_name", "item_number", "quantity"]
def extract_ipn_items_params
mod_params = Hash.new{|k, v| k[v] = {} }
ITEM_PARAM_PREFIXES.each do |item_data_key|
key_tracker = 1
loop do
current_key = (item_data_key + key_tracker.to_s).to_sym
if params.include? current_key
mod_params[key_tracker][item_data_key] = params[current_key]
else
break
end
key_tracker += 1
end
end
mod_params
end
The method returns a hash of hashes like:
{1 => {item_name: 'Item 1', item_number: 1084, quantity: 15}}, if you have nested attributes set up for a user, I think you should be able to do something like, not really sure, but should be possible:
user.update(payment_notifications_attributes: extract_ipn_items_params)
Let me know if that works for you.
UPDATE
Based on the Github Gist, here's something I was able to come up with:
class PaymentNotificationsController < ApplicationController
protect_from_forgery except: [:create]
ITEM_PARAM_PREFIXES = ["item_name", "item_number", "quantity", "option_name"]
def create
extract_ipn_items_params.each do |key, values|
# this approach loops through all the returned results, nested attributes may help abstract this though
PaymentNotification.create(values)
render nothing: true
end
def details
# params.extract_ipn_items_params #this doesn't exist as params is an instance of ActionController::Parameters
PaymentNotification.update_attributes(line_item_id: params[:item_number], product_title: params[:item_name], option_name: params[:option_name], quantity: params[:quantity])
end
private
def additional_attributes
# create this for additional merge attributes. A better place for these would be the parent of this
{
params: params,
cart_id: params[:invoice],
status: params[:payment_status],
transaction_id: params[:txn_id],
first_name: params[:first_name],
last_name: params[:last_name],
email: params[:payer_email],
address_name: params[:address_name],
address_street: params[:address_street],
address_city: params[:address_city],
address_state: params[:address_state],
address_zip: params[:address_zip],
address_country: params[:address_country]
}
end
def extract_ipn_items_params
mod_params = Hash.new{|k, v| k[v] = {}.merge(additional_attributes) }
ITEM_PARAM_PREFIXES.each do |item_data_key|
key_tracker = 1
loop do
current_key = (item_data_key + key_tracker.to_s).to_sym
if params.include? current_key
mod_params[key_tracker][item_data_key] = params[current_key]
else
break
end
key_tracker += 1
end
end
mod_params
end
end
Let me know if that fixes your problem.
You should have payment_id so you can find it by using gem 'paypal-sdk-rest'
payment = PayPal::SDK::REST::Payment.find payment_id
then you could see all details in payment object

Why Rails deserializes snake case data structures but serializes camel case data structures?

It is one of my first Ruby on Rails project and it is weird for me to send JSON with properties written in snake case on my requests and receive JSON with properties written in camel case on my responses.
Here is an example of request payload:
{
"end_point":"test"
}
And here is an example of response payload:
{
"endPoint":"test"
}
Here is the code that consumes and returns the alike data structures above:
def create
def create
#api = interactor.create(params[:organization_id], api_params)
respond_to do |format|
format.json { render :show, status: :created, location: api_url(#api) }
end
end
Use String#camelize method from ActiveSupport(which comes with Rails) like so:
attributes_hash = { "test_data" => "test" }
json_hash = Hash[attributes_hash.map {|k, v| [k.camelize(:lower), v] }]
#=> {"testData"=>"test"}
Now, you can convert it to JSON string:
p json_hash.to_json #=> "{\"testData\":\"test\"}"
UPDATE: You can override serializable_hash method in your model class(since you're not clear with what exactly interceptor is in your code, I assume you'd need to put this in the class whose object you're instantiating to send the data as JSON) like so -
def serializable_hash(options = nil)
options ||= {}
if options[:camelize]
Hash[attributes.map {|k, v| [k.camelize(:lower), v] }]
else
super
end
end
Now, you can do: #api.to_json(:camelize => true) and it'll convert attributes to camecase except the first character, i.e.: endPoint.

Resources