I'm trying to write spec for testing upload function and the code implementation works as expected however when I tried to write spec I'm not able to figure out why the data conversation is failing during JSON.parse. [ Rails 5.X ]
Method
def upload
#some validation
begin
puts params[:file]
json = JSON.parse(params[:file].read)
#rest of the validation
rescue StandardError, JSON::ParserError, HttpServices::BadHttpResponseError
flash[:style] = :error
end
end
Spec:
describe "upload" do
before do
read = file_fixture("empy_details.json").read
#file = Hash.new
#file['emp'] = read #debugger > #file:{emp: [{"name":"Bob","key":"201","active":true}]}
end
it 'should upload' do
post :upload, params: { :file => #file }, as: :json
expect(flash[:style]).to eq(:success)
end
end
The method puts params[:file] prints
{"emp"=>"[{\"name\":\"Bob\",\"key\":\"201\",\"active\":true}]\n"}
The JSON.parse fails at convert_hashes_to_parameters(key, value) method
and converted gets value of "[{"name":"Bob","key":"201","active":true}]" before failing.
What am I missing ?
params[:file].read was throwing exception when the file was passed through Rspec and I changed the controller method code to accommodate params[:file] instead.
def upload
#some validation
begin
puts params[:file]
if params[:file].respond_to?(:read)
json = JSON.parse(params[:file].read)
else
json = JSON.parse(params[:file])
end
#rest of the validation
rescue StandardError, JSON::ParserError, HttpServices::BadHttpResponseError
flash[:style] = :error
end
end
Related
I am trying to write the allow method in RSpec. My rails controller is
module Users
class ProfilesController < ApplicationController
# Update user profile
def update
payload = { name: params[:user][:name],email: params[:user][:email]}
response = send_request_to_update_in_company(payload)
if response['code'] == 200
if User.first.update(user_params)
render json: { message: "User successfully updated"}, status: :ok
else
head :unprocessable_entity
end
else
render json: { error: 'Error updating user in Company' },status: :unprocessable_entity
end
end
private
def send_request_to_update_in_comapny(payload)
response = Api::V1::CompanyRequestService.new(
payload: payload.merge(company_api_access_details),
url: 'customers/update_name_email',
request_method: Net::HTTP::Post
).call
JSON.parse(response.body)
end
end
end
When I write the bellow code in my test file
allow(Users::ProfilesController).to receive(:send_request_to_update_in_company).and_return({ 'code' => 500 })
I am getting the following error in terminal
Users::ProfilesController does not implement: send_request_to_update_in_comapny
enter code here
With allow_any_instance_of I am able to get the code working. But how can I implement it using allow?
Yes, allow_any_instance_of works because, as the name suggests, it allows any instance of Users::ProfilesController to respond to the instance method send_request_to_update_in_company with your mock return value.
However, your line
allow(Users::ProfilesController).to receive(:send_request_to_update_in_company)
is telling RSpec to mock a class method called send_request_to_update_in_company, which doesn't exist. And so, you're seeing the error message saying so.
You don't say where your test is situated, but generally wherever it is, it's not a good idea to either test or stub out a private method.
I'd be inclined to instead create a mock Api::V1::CompanyRequestService object to return a fake response, which your controller code can then parse as expected and produce the expected JSON. For example
mock_request = instance_double(Api::V1::CompanyRequestService)
allow(mock_request).to receive(:call).and_return('{"code": 500}')
allow(Api::V1::CompanyRequestService).to receive(:new).and_return(mock_request)
Another approach might be to leave your service alone, and instead use tools like VCR or WebMock to provide mocked JSON values at the network layer - your code can think it's calling out to the internet, but really it gets back responses that you define in your tests.
How about this way:
spec/requests/users/profiles_controller_spec.rb
require 'rails_helper'
RSpec.describe "Users::ProfilesControllers", type: :request do
describe "Test call to special function: " do
let(:controller) { Users::ProfilesController.new }
it "Should response to code 500" do
response = controller.send_request_to_update_in_company("test")
expect(response).to eq({"code"=>"500", "test1"=>"abc", "test2"=>"def"})
end
it "Should return to true" do
response = controller.true_flag?
expect(response).to eq(true)
end
end
end
app/controllers/users/profiles_controller.rb
module Users
class ProfilesController < ApplicationController
# Update user profile
def update
payload = { name: params[:user][:name],email: params[:user][:email]}
response = send_request_to_update_in_company(payload)
Rails.logger.debug "Ok71 = response['code'] = #{response['code']}"
# if response['code'] == 200
# if User.first.update(user_params)
# render json: { message: "User successfully updated"}, status: :ok
# else
# head :unprocessable_entity
# end
# else
# render json: { error: 'Error updating user in Company' },status: :unprocessable_entity
# end
end
# Not private, and not mistake to 'send_request_to_update_in_comapny'
def send_request_to_update_in_company(payload)
response = Api::V1::CompanyRequestService.new(
payload: "for_simple_payload_merge_values",
url: 'for_simple_customers/update_name_email',
request_method: "for_simple_request_method"
).call
Rails.logger.debug "Ok66 = Start to log response"
Rails.logger.debug response
JSON.parse(response.body)
end
# Simple function to test
def true_flag?
true
end
end
end
app/services/api/v1/company_request_service.rb
class Api::V1::CompanyRequestService < ActionController::API
def initialize(payload="test1", url="test2", request_method="test3")
#payload = payload
#url = url
#request_method = request_method
end
def call
#object = Example.new
#object.body = {code: "500", test1: "abc", test2: "def"}.to_json
return #object
end
end
class Example
attr_accessor :body
def initialize(body={code: "000", test1: "init_value_abc", test2: "init_value_def"}.to_json)
#body = body
end
end
I use simple code to simulate your project. Modify it to suitable your working! Tell me about your its thinking. Thank you!
I want to send_data to the admin who is using the activeadmin interface in our website. This data is a zip file and can be downloaded if certain conditions on the selected items are met.
I created a service that handles the logic (quite complex) behind it. So from activeadmin I can call:
batch_action :action_name, form: {selection: ['...']} do |ids, inputs|
response = MyService.new(ids, inputs[:selection]).my_method
redirect_to collection_path
end
In my service MyService.rb:
...
def my_method
...
if condition
zip_data = Zip::OutputStream.write_buffer do |zip|
zip.put_next_entry("#{original_file_name}.xml")
zip << File.read(original_file)
end
send_data(zip_data.read, :type => 'application/zip', :filename => "#{original_file_name}.zip")
# here send_data throws an error because it's a controller method
else
...
end
...
end
...
But how do I use the send_data method properly? Maybe I have to restructure something? I know you can probably do ActionController::DataStreaming.send_data(...) outside of the controller, but this is not recommended for the code's sake.
Solved. I put the send_datain the batch_action code like this:
batch_action :action_name, form: {selection: ['...']} do |ids, inputs|
response = MyService.new(ids, inputs[:selection]).my_method
redirect_to collection_path
send_data(response[:zip][:data].read, :type => 'application/zip', :filename => "#{response[:zip][:name]}.zip") if response[:zip].present?
end
where the response contains the zip data to send (which needs to be rewinded with zip_data.rewind before being sent). my_service.rb is now like:
...
def my_method
...
if condition
zip_data = Zip::OutputStream.write_buffer do |zip|
zip.put_next_entry("#{original_file_name}.xml")
zip << File.read(original_file)
end
zip_data.rewind
response[:zip] = {data: zip_data, name: original_file_name}
else
...
end
...
end
...
In my rails application I have a module with an API call that I would like to write rspec tests for. What would you write as proper tests for this? I have read not to combine httparty with rspec because it is very slow and now I have no idea how to do this. Thanks for the advice!
module BandsInTown
class API
attr_reader :artist_name, :info, :events
def initialize(artist_name)
#artist_name = artist_name
end
def artist_info
#info = fetch_data
end
def artist_events
#events = fetch_data 'events'
end
private
def fetch_data(endpoint = nil)
request_url = "http://api.bandsintown.com/artists/#{[#artist_name, endpoint].compact.join('/')}.json?api_version=2.0&app_id=AUTH_KEY"
resp = HTTParty.get(request_url)
return false if resp.code != 200
resp
end
end
end
I would use webmock to get around making API requests.
Here's an example on how the code in yout spec_helper.rb could look like:
require 'webmock/rspec'
def stub_api_requests
response = File.read('spec/fixtures/payload.json')
stub_request(:get, %r{http:\/\/api.bandsintown.com\/artists/\/(.)*})
.to_return(
status: 200,
body: response
)
end
config.before(:each) do
stub_api_requests
end
I keep getting a 422 error when testing stripe's webhook for customer.subscription.deleted
I placed this in my config routes
post 'stripewebhooks/receive'
here is my controller
class StripewebhooksController < ApplicationController
Stripe::api_key = ENV['STRIPE_SECRET_KEY']
require 'json'
def receive
data_json = JSON.parse request.body.read
p data_json['data']['object']['customer']
if data_json[:type] == "customer.subscription.deleted"
cancel_subscription(data_event)
end
end
def cancel_subscription(data_event)
#subscription = Subscription.find_by_stripe_customer_token(data['data']['object']['customer'])
#subscription.update_attribute(:subscription_status, "inactive")
end
end
I am unclear on what is suppose to go in the parenthesis after
def cancel_subscription
I am not sure that I am suppose to put data_event or what this means.
When you get a post data from stripe, you need to return a 200 status code from your application.
try this
def receive
data_json = JSON.parse request.body.read
p data_json['data']['object']['customer']
if data_json[:type] == "customer.subscription.deleted"
# Why did you send data_event? send the parsed data_json as parameter
cancel_subscription(data_json)
end
# Return a 200 status code
render :text => '{}', :status => :ok
end
Rails 4 adds an exception ActionDispatch::ParamsParser::ParseError exception but since its in the middleware stack it appears it can't be rescued in the normal controller environment. In a json API application I want respond with a standard error format.
This gist shows a strategy for inserting middleware to intercept and respond. Following this pattern I have:
application.rb:
module Traphos
class Application < Rails::Application
....
config.middleware.insert_before ActionDispatch::ParamsParser, "JSONParseError"
end
end
And the middleware is:
class JSONParseError
def initialize(app)
#app = app
end
def call(env)
begin
#app.call(env)
rescue ActionDispatch::ParamsParser::ParseError => e
[422, {}, ['Parse Error']]
end
end
end
If I run my test without the middleware I get (spec):
Failures:
1) Photo update attributes with non-parseable json
Failure/Error: patch update_url, {:description => description}, "CONTENT_TYPE" => content_type, "HTTP_ACCEPT" => accepts, "HTTP_AUTHORIZATION" => #auth
ActionDispatch::ParamsParser::ParseError:
399: unexpected token at 'description=Test+New+Description]'
Which is exactly what I would expect (ParseError that I can't rescue_from).
Now with the only change to add in the middleware above:
2) Photo update attributes with non-parseable json
Failure/Error: response.status.should eql(422)
expected: 422
got: 200
And the log shows that the standard controller action is being executed and returning a normal response (although since it didn't receive any parameters it didn't update anything).
My questions:
How can rescue from ParseError and return a custom response. Feels like I'm on the right track but not quite there.
I can't work out why, when the exception is raised and rescued, that the controller action still proceeds.
Help much appreciated, --Kip
Turns out that further up the middleware stack, ActionDispatch::ShowExceptions can be configured with an exceptions app.
module Traphos
class Application < Rails::Application
# For the exceptions app
require "#{config.root}/lib/exceptions/public_exceptions"
config.exceptions_app = Traphos::PublicExceptions.new(Rails.public_path)
end
end
Based heavily on the Rails provided one I am now using:
module Traphos
class PublicExceptions
attr_accessor :public_path
def initialize(public_path)
#public_path = public_path
end
def call(env)
exception = env["action_dispatch.exception"]
status = code_from_exception(env["PATH_INFO"][1..-1], exception)
request = ActionDispatch::Request.new(env)
content_type = request.formats.first
body = {:status => { :code => status, :exception => exception.class.name, :message => exception.message }}
render(status, content_type, body)
end
private
def render(status, content_type, body)
format = content_type && "to_#{content_type.to_sym}"
if format && body.respond_to?(format)
render_format(status, content_type, body.public_send(format))
else
render_html(status)
end
end
def render_format(status, content_type, body)
[status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
'Content-Length' => body.bytesize.to_s}, [body]]
end
def render_html(status)
found = false
path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale
path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path))
if found || File.exist?(path)
render_format(status, 'text/html', File.read(path))
else
[404, { "X-Cascade" => "pass" }, []]
end
end
def code_from_exception(status, exception)
case exception
when ActionDispatch::ParamsParser::ParseError
"422"
else
status
end
end
end
end
To use it in a test environment requires setting config variables (otherwise you get the standard exception handling in development and test). So to test I have (edited to just have the key parts):
describe Photo, :type => :api do
context 'update' do
it 'attributes with non-parseable json' do
Rails.application.config.consider_all_requests_local = false
Rails.application.config.action_dispatch.show_exceptions = true
patch update_url, {:description => description}
response.status.should eql(422)
result = JSON.parse(response.body)
result['status']['exception'].should match(/ParseError/)
Rails.application.config.consider_all_requests_local = true
Rails.application.config.action_dispatch.show_exceptions = false
end
end
end
Which performs as I need in a public API way and is adaptable for any other exceptions I may choose to customise.
This article (also from 2013) thoughtbot covers also this topic. They put their response inside this middleware service only if you requested json
if env['HTTP_ACCEPT'] =~ /application\/json/
error_output = "There was a problem in the JSON you submitted: #{error}"
return [
400, { "Content-Type" => "application/json" },
[ { status: 400, error: error_output }.to_json ]
]
else
raise error
end