I am very new to rails and I am writing test cases for my rails application.
Suppose I have a code like this -
class EmailController < ApplicationController
def new
render :layout => false
end
def create
begin
process_email = ProcessEmail.new(params)
process_email.perform
rescue EmailHelper::HtmlSanitizerTimeoutError, EmailHelper::NokogiriTimeoutError => e
NewRelic::Agent.notice_error(e, account_id: Account.current.id, subject: params[:subject])
process_email.mark_email_on_timeout
end
render layout: 'email'
end
def update
----
end
I have written test cases for create and update. But how do I write
test cases that will execute the rescue part??
This is my db
(class abc < ActiveRecord::Base)
this is my controller class
(class abcsController < ApiApplicationController).
I have written minitest like this `
def test_raise_exception
params = {"a":"b"}
item = abc.new(params)
raises_exception = -> { raise ArgumentError.new }
item.stub :create, raises_exception do
assert_raises(ArgumentError) { post :create }
end
end`
This is error which is coming -
NameError: NameError: undefined method create' for class
I know this questions is asked several times but I checked those answers but nothing seems to fix my issue.
users_controller.rb on path app/controllers/api/v1/
module Api
module V1
class UsersController < ApplicationController
def create_user
#users = User.new(user_params)
if #users.save
render json: { status: '201', message: 'User created successfully' }
else
render json: { status: '400', message: 'Invalid user info', data: #users.errors }
end
end
def user_params
params.permit(:first_name, :last_name, :email, :password)
end
end
end
end
routes.rb
namespace 'api' do
namespace 'v1' do
post "user/createuser", to: "user#create_user"
end
end
What I have tried:
Checked the directory structure it is as mentioned
Restarted the server and checked
Folder name: all simple controllers > api > v1
But this works fine when I changed the routes.rb
post "user/createuser", to: "user#create_user"
to resource :users
and
def create_user
to def create
Why things does not work when I define custom routes instead of using default routes? How to get this work with custom routes
Due to Rails conventions I believe you need to update your route with
post "user/createuser", to: "users#create"
instead of
post "user/createuser", to: "user#create_user"
I want the URL to be changed when a call to an action within the controller is made. My current scenario:
Controller:
class EventController < ApplicationController
def index
if city.blank?
failure
end
...
end
def failure
...
render 'failure'
end
end
Routes:
get '/event', to: 'event#index'
post '/event/failure' => 'event#failure'
But this code keeps the url as /events. The desired result is /events/failure
I've views for payment 'index' and 'failure'. I'm using rails ~ 5.0.0.
For the URL to change you will want to do a redirect, something like this :
class EventController < ApplicationController
def index
if city.blank?
redirect_to action: 'failure'
end
...
end
def failure
...
render 'failure'
end
end
But, redirection is not possible for POST requests as given in HTTP/1.1
You might want to consider changing your strategy.
I am unable to override the Rails serializer when using devise_token_auth and active_model_serializer for Devise sign_up method.
I would like to customize the returned fields from the Devise sign_up controller when querying my API.
The devise_token_auth gem documentation indicates:
To customize json rendering, implement the following protected controller methods
Registration Controller
...
render_create_success
...
Note: Controller overrides must implement the expected actions of the controllers that they replace.
That is all well and good, but how do I do this?
I've tried generating a UserController serializer like the following:
class UsersController < ApplicationController
def default_serializer_options
{ serializer: UserSerializer }
end
# GET /users
def index
#users = User.all
render json: #users
end
end
but it's only being used for custom methods such as the index method above: it's not being picked up by devise methods like sign_up
I would appreciate a detailed response since I've looked everywhere but I only get a piece of the puzzle at a time.
For the specific serialiser question, here's how I did it:
overrides/sessions_controller.rb
module Api
module V1
module Overrides
class SessionsController < ::DeviseTokenAuth::SessionsController
# override this method to customise how the resource is rendered. in this case an ActiveModelSerializers 0.10 serializer.
def render_create_success
render json: { data: ActiveModelSerializers::SerializableResource.new(#resource).as_json }
end
end
end
end
end
config/routes.rb
namespace :api, defaults: {format: 'json'} do
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'api/v1/overrides/sessions'
}
# snip the rest
Devise sign_up corresponds to devise_token_auth registrations controller and Devise sign_in corresponds to devise_token_auth sessions controller. Therefore when using this gem, customizing Devise sign_in and sign_up methods requires customizing both of these devise_token_auth controllers.
There are two ways to go about this based on what you need to accomplish.
Method #1
If you want to completely customize a method in the controller then follow the documentation for overriding devise_token_auth controller methods here: https://github.com/lynndylanhurley/devise_token_auth#custom-controller-overrides
This is what I did and it's working fine:
#config/routes.rb
...
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'overrides/sessions',
registrations: 'overrides/registrations'
}
...
This will route all devise_token_auth sessions and registrations to LOCAL versions of the controllers if a method exists in your local controller override. If the method does not exist in your local override, then it will run the method from the gem. You basically have to copy the controllers from the gem into 'app/controllers/overrides' and make any changes to any method you need to customize. Erase the methods from the local copy you are not customizing. You can also add callbacks in this way. If you want to modify the response, customize the the render at the end of the method that will return the response as json via active_model_serializer.
This is an example of my sessions controller which adds a couple of custom before_actions to add custom functionality:
#app/controllers/overrides/sessions_controller.rb
module Overrides
class SessionsController < DeviseTokenAuth::SessionsController
skip_before_action :authenticate_user_with_filter
before_action :set_country_by_ip, :only => [:create]
before_action :create_facebook_user, :only => [:create]
def create
# Check
field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
#resource = nil
if field
q_value = resource_params[field]
if resource_class.case_insensitive_keys.include?(field)
q_value.downcase!
end
#q = "#{field.to_s} = ? AND provider='email'"
q = "#{field.to_s} = ? AND provider='#{params[:provider]}'"
#if ActiveRecord::Base.connection.adapter_name.downcase.starts_with? 'mysql'
# q = "BINARY " + q
#end
#resource = resource_class.where(q, q_value).first
end
#sign in will be successful if #resource exists (matching user was found) and is a facebook login OR (email login and password matches)
if #resource and (params[:provider] == 'facebook' || (valid_params?(field, q_value) and #resource.valid_password?(resource_params[:password]) and (!#resource.respond_to?(:active_for_authentication?) or #resource.active_for_authentication?)))
# create client id
#client_id = SecureRandom.urlsafe_base64(nil, false)
#token = SecureRandom.urlsafe_base64(nil, false)
#resource.tokens[#client_id] = { token: BCrypt::Password.create(#token), expiry: (Time.now + DeviseTokenAuth.token_lifespan).to_i }
#resource.save
sign_in(:user, #resource, store: false, bypass: false)
yield #resource if block_given?
#render_create_success
render json: { data: resource_data(resource_json: #resource.token_validation_response) }
elsif #resource and not (!#resource.respond_to?(:active_for_authentication?) or #resource.active_for_authentication?)
render_create_error_not_confirmed
else
render_create_error_bad_credentials
end
end
def set_country_by_ip
if !params['fb_code'].blank?
if !params['user_ip'].blank?
#checks if IP sent is valid, otherwise raise an error
raise 'Invalid IP' unless (params['user_ip'] =~ Resolv::IPv4::Regex ? true : false)
country_code = Custom::FacesLibrary.get_country_by_ip(params['user_ip'])
country_id = Country.find_by(country_code: country_code)
if country_id
params.merge!(country_id: country_id.id, country_name: country_id.name, test: 'Test')
I18n.locale = country_id.language_code
else
params.merge!(country_id: 1, country_name: 'International')
end
else
params.merge!(country_id: 1, country_name: 'International')
end
end
end
def create_facebook_user
if !params['fb_code'].blank?
# TODO capture errors for invalid, expired or already used codes to return beter errors in API
user_info, access_token = Omniauth::Facebook.authenticate(params['fb_code'])
if user_info['email'].blank?
Omniauth::Facebook.deauthorize(access_token)
end
#if Facebook user does not exist create it
#user = User.find_by('uid = ? and provider = ?', user_info['id'], 'facebook')
if !#user
#graph = Koala::Facebook::API.new(access_token, ENV['FACEBOOK_APP_SECRET'])
Koala.config.api_version = "v2.6"
new_user_picture = #graph.get_picture_data(user_info['id'], type: :normal)
new_user_info = {
uid: user_info['id'],
provider: 'facebook',
email: user_info['email'],
name: user_info['name'],
first_name: user_info['first_name'],
last_name: user_info['last_name'],
image: new_user_picture['data']['url'],
gender: user_info['gender'],
fb_auth_token: access_token,
friend_count: user_info['friends']['summary']['total_count'],
friends: user_info['friends']['data']
}
#user = User.new(new_user_info)
#user.password = Devise.friendly_token.first(8)
#user.country_id = params['country_id']
#user.country_name = params['country_name']
if !#user.save
render json: #user.errors, status: :unprocessable_entity
end
end
#regardless of user creation, merge facebook parameters for proper sign_in in standard action
params.merge!(provider: 'facebook', email: #user.email)
else
params.merge!(provider: 'email')
end
end
end
end
Notice the use of params.merge! in the callback to add custom parameters to the main controller methods. This is a nifty trick that unfortunately will be be deprecated in Rails 5.1 as params will no longer inherit from hash.
Method #2
If you just want to add functionality to a method in your custom controller, you can get away with subclassing a controller, inheriting from the original controller and passing a block to super as described here:
https://github.com/lynndylanhurley/devise_token_auth#passing-blocks-to-controllers
I have done this to the create method in my custom registrations controller.
Modify the routes as in method #1
#config/routes.rb
...
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'overrides/sessions',
registrations: 'overrides/registrations'
}
...
and customize the create method in the custom controller:
#app/controllers/overrides/registrations_controller.rb
module Overrides
class RegistrationsController < DeviseTokenAuth::RegistrationsController
skip_before_action :authenticate_user_with_filter
#will run upon creating a new registration and will set the country_id and locale parameters
#based on whether or not a user_ip param is sent with the request
#will default to country_id=1 and locale='en' (International) if it's not sent.
before_action :set_country_and_locale_by_ip, :only => [:create]
def set_country_and_locale_by_ip
if !params['user_ip'].blank?
#checks if IP sent is valid, otherwise raise an error
raise 'Invalid IP' unless (params['user_ip'] =~ Resolv::IPv4::Regex ? true : false)
country_code = Custom::FacesLibrary.get_country_by_ip(params['user_ip'])
#TODO check if there's an internet connection here or inside the library function
#params.merge!(country_id: 1, country_name: 'International', locale: 'en')
country_id = Country.find_by(country_code: country_code)
if country_id
params.merge!(country_id: country_id.id, locale: country_id.language_code, country_name: country_id.name)
else
params.merge!(country_id: 1, country_name: 'International', locale: 'en')
end
else
params.merge!(country_id: 1, country_name: 'International', locale: 'en')
end
end
#this will add behaviour to the registrations controller create method
def create
super do |resource|
create_assets(#resource)
end
end
def create_assets(user)
begin
Asset.create(user_id: user.id, name: "stars", qty: 50)
Asset.create(user_id: user.id, name: "lives", qty: 5)
Asset.create(user_id: user.id, name: "trophies", qty: 0)
end
end
end
end
We have a custom exception app that has been raising (fail safe) exceptions (the application equivalent of having an exception in a rescue block).
I think I've fixed it, but am finding it hard to test. It's an unrouted controller, so I can't use controller tests (require routing).
i.e. I have Rails.configuration.exceptions_app = ExceptionController.action(:show), not Rails.configuration.exceptions_app = self.routes.
Basically what I think I need to do is
Generate a test request request = ActionDispatch::TestRequest.new
include Rack::Test or maybe mimic behavior in ActiveSupport::IntegrationTest
Set #app = ExceptionsController.action(:show)
Fake an exception request.env.merge! 'action_dispatch.exception' => ActionController::RoutingError.new(:foo)
Test response = #app.call(request.env)
Assert no exception is raised and correct response body and status
Problems:
The env needs
a warden / devise session with current_user request.env['warden'] = spy(Warden) and request.session = ActionDispatch::Integration::Session.new(#app)
to manipulate request formats so that I can check that a request without an accept defaults to json request.any?(:json)? constraints: { default: :json } ? `request.accept = "application/javascript"
work work with the respond_with responder
set action_dispatch.show_exceptions, consider all requests local, etc request.env["action_dispatch.show_detailed_exceptions"] = true
Also, I considered building a ActionDispatch::ShowException.new(app, ExceptionController.new) or a small rack app
But our gem has no tests and I haven't been able to apply anything that I've read in exception handling gems (most work at the rescue_action_in_public level or mix in to ShowException) or in the Rails source code
This is a Rails 4.2 app tested via Rspec and Capybara.
Thoughts, links, halp?
Example code and tests
RSpec.describe 'ExceptionController' do
class ExceptionController < ActionController::Base
use ActionDispatch::ShowExceptions, Rails.configuration.exceptions_app
use ActionDispatch::DebugExceptions
#Response
respond_to :html, :json
#Layout
layout :layout_status
#Dependencies
before_action :status, :app_name, :log_exception
def show
respond_with details, status: #status, location: nil
end
def show_detailed_exceptions?
request.local?
end
protected
####################
# Dependencies #
####################
#Info
def status
#exception = env['action_dispatch.exception']
#status = ActionDispatch::ExceptionWrapper.new(env, #exception).status_code
#response = ActionDispatch::ExceptionWrapper.rescue_responses[#exception.class.name]
end
#Format
def details
#details ||= {}.tap do |h|
I18n.with_options scope: [:exception, :show, #response], exception_name: #exception.class.name, exception_message: #exception.message do |i18n|
h[:name] = i18n.t "#{#exception.class.name.underscore}.title", default: i18n.t(:title, default: #exception.class.name)
h[:message] = i18n.t "#{#exception.class.name.underscore}.description", default: i18n.t(:description, default: #exception.message)
end
end
end
helper_method :details
####################
# Layout #
####################
private
def log_exception
if #status.to_s == '500'
request.env[:exception_details] = details
request.env[:exception_details][:location] = ActionDispatch::ExceptionWrapper.new(env, #exception).application_trace[0]
end
end
#Layout
def layout_status
#status.to_s != '404' ? 'error' : 'application'
end
#App
def app_name
#app_name = Rails.application.class.parent_name
end
end
include Rack::Test::Methods
include ActionDispatch::Integration::Runner
include ActionController::TemplateAssertions
include ActionDispatch::Routing::UrlFor
let(:exception) { ActionController::RoutingError.new(:foo) }
let(:request) { ActionDispatch::TestRequest.new }
def app
# Rails.application.config.exceptions_app
#app ||= ExceptionController.action(:show)
end
it 'logs unknown format errors' do
request.env['action_dispatch.show_exceptions'] = true
request.env['consider_all_requests_local'] = true
request.env['warden'] = spy(Warden)
request.session = ActionDispatch::Integration::Session.new(app)
exception = ActionController::RoutingError.new(:foo)
request.env.merge! 'action_dispatch.exception' => exception
post '/whatever'
expect(response.body).to eq("dunno?")
end
end
refs:
https://github.com/richpeck/exception_handler
https://github.com/plataformatec/responders/blob/8f03848a2f50d4685c15a31254a1f600af947bd7/test/action_controller/respond_with_test.rb#L265-L275
https://github.com/rails/rails/blob/1d43458c148f9532a81b92ee3a247da4f1c0b7ad/actionpack/test/dispatch/show_exceptions_test.rb#L92-L99
https://github.com/rails/rails/blob/3e36db4406beea32772b1db1e9a16cc1e8aea14c/railties/test/application/middleware/exceptions_test.rb#L86-L91
https://github.com/rails/rails/blob/34fa6658dd1b779b21e586f01ee64c6f59ca1537/actionpack/lib/action_dispatch/testing/integration.rb#L647-L674
https://github.com/rails/rails/blob/ef8d09d932e36b0614905ea5bc3fb6af318b6ce2/actionview/test/abstract_unit.rb#L146-L184
https://github.com/plataformatec/responders/blob/8f03848a2f50d4685c15a31254a1f600af947bd7/lib/action_controller/respond_with.rb#L196-L207
http://blog.plataformatec.com.br/2012/01/my-five-favorite-hidden-features-in-rails-3-2/
https://github.com/bugsnag/bugsnag-ruby/blob/master/lib/bugsnag/middleware/warden_user.rb
https://github.com/bugsnag/bugsnag-ruby/blob/master/lib/bugsnag/rails/action_controller_rescue.rb#L3-L33
https://github.com/bugsnag/bugsnag-ruby/blob/master/lib/bugsnag/rails/active_record_rescue.rb
https://github.com/rollbar/rollbar-gem/blob/master/spec/rollbar_spec.rb
http://andre.arko.net/2011/12/10/make-rails-3-stop-trying-to-serve-html/
https://github.com/rails/rails/blob/9503e65b9718e4f01860cf017c1cdcdccdfffde7/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L46-L49
Update 2015-08-27
It has been suggested that this question may be a duplicate of Testing error pages in Rails with Rspec + Capybara, however, that question addresses testing exception responses when the exceptions_app is set to routes.
As I wrote above, I'm using a Controller as the exceptions_app, so though I could use capybara to visit non-existing pages, I'd like to test Controller's action directly, rather than include the rest of the show exceptions stack. This is important because my problem is when the exceptions app is called with an unhandled content type, which I cannot easily test via capybara.
More generally, what I need to test is when the Exceptions app raises and exception, that I have fixed it.
I'm open to seeing some example code, though.