how to TDD deleting a user in devise - ruby-on-rails

I setup Devise so I can write controller specs with this.
Then I setup Devise so users cannot delete their accounts.
Now I want to write a spec to make sure the controller is unable to call the destroy action on the Devise user. How do I write this?
In my controller the Devise part looks like this
devise_for :users, skip: :registrations do
resource :registration,
only: [:new, :create, :edit, :update],
path: 'users',
path_names: { new: 'sign_up' },
controller: 'devise/registrations',
as: :user_registration do
get :cancel
end
end
In my spec I'm trying to do the following but it doesn't work. I'm not even sure I'm writing it right. I think the page I'm trying to access is wrong.
describe UsersController do
login_user # from devise controller helper
it "does not allow deleting of user" do
get :users, :method => :delete
# assert here user was not deleted
end
end

I think what you really want to test is whether or not the route exists for the registrations#destroy action. If there is no route, then the action will not be called since it can't be routed to the controller.
For a destroy action, we need to try to route a DELETE action to the users path. so, something like this might do the trick:
{ :delete=> "/users" }.should_not be_routable
Test syntax pulled from a similar answer here:
Rails RSpec Routing: Testing actions in :except do NOT route

Your mixing your http verbs for one thing. You should be doing
delete :destroy, id: #user
Your going to have to get #user from somewhere, I have it set by controller macros personally.
Then you can either check the response header for unsuccessful, or more easily
#user.should exist

I would put the following in my controller spec when testing this kind of thing (although i'd use FactoryGirl to create my test user):
it "does not allow deletion of a user" do
user = User.create!([insert valid args here])
expect {
delete :destroy, id: user
}.not_to change(User, :count)
end

Related

Could not find devise mapping for controller path

I know there are a ton of questions with this same problem but none of the solutions seem to be working for my situation.
I've defined my own sessions, registrations, and users controllers since I'm using an API with token authentication, but I can't seem to get my routes/scoping working.
I get the following error message upon trying to run an Spec test on my sessions controller (which inherits from DeviseController)
Failure/Error: post :create, credentials
AbstractController::ActionNotFound:
Could not find devise mapping for path "/api/v1/sessions/create?user_login%5Bemail%5D=rosina%40russel.name&user_login%5Bpassword%5D=12345678".
This may happen for two reasons:
1) You forgot to wrap your route inside the scope block. For example:
devise_scope :user do
get "/some/route" => "some_devise_controller"
end
2) You are testing a Devise controller bypassing the router.
If so, you can explicitly tell Devise which mapping to use:
#request.env["devise.mapping"] = Devise.mappings[:user]
I've actually done both of these things based on other answers that I've read, but I don't think its done correctly for my specific situation.
Here is the spec test I'm trying to run
describe "POST #create" do
before(:each) do
#user = FactoryGirl.create :user
#user.skip_confirmation!
#request.env['devise.mapping'] = Devise.mappings[:user]
end
context "when the credentials are correct" do
before(:each) do
credentials = { user_login: { :email => #user.email, :password => "12345678"} }
post :create, credentials
end
it "returns the user record corresponding to the given credentials" do
#user.reload
user_response = JSON.parse(response.body, symbolize_names: true)
expect(user_response[:auth_token]).to eql #user.auth_token
end
it { should respond_with :created }
end
end
As you can see, I'm specifying what it's saying that I haven't specified.
Here is the snippet from my routes.rb before the definition of my API in it's namespace:
namespace :api, defaults: { format: :json } do
namespace :v1 do
devise_for :users, skip: [:sessions, :registrations]
devise_scope :user do
post 'sessions', to: 'sessions#create'
delete 'sessions', to: 'sessions#destroy'
end
If anyone sees anything from with this please let me know, I'd love to test my controller...
And yes, my sessions_controller is located in app/controllers/api/v1/sessions_controller.rb and it is properly defined as class API::V1::SessionsController < DeviseController
I noticed that devise_for is supposed to SET Devise.mapping but that seems to not be happening at all!
I should've done this right away. I sent Devise.mappings.inspect to the logger and it read that the mapping was stored in :api_v1_user so the key in the mapping hash corresponds to the namespace that you wrote devise_for in... hopefully this will help others
Based on your routes, API is in json format. So you need to specify that when sending post method. Which is why I think rspec is complaining about that route not existing because it doesn't know to use json. Try changing this
post :create, credentials
to this
post :create, credentials, format: :json

"resources :post, except: :new" makes Rails think the route /posts/new would point to a post ID "new"

I have the following route:
resources :success_criteria, except: :new
The following spec fails:
describe SuccessCriteriaController do
describe 'routing' do
it 'routes to #new' do
expect(get('/success_criteria/new')).to_not be_routable
end
end
end
Failure message:
Failure/Error: expect(get('/posts/new')).to_not be_routable
expected {:get=>"/posts/new"} not to be routable, but it routes to {:controller=>"posts", :action=>"show", :id=>"new"}
The controller looks like this:
class SuccessCriteriaController < InheritedResources::Base
load_and_authorize_resource
end
Why does Rails think that posts/new would point to a post with the ID new? That's not right, is it? Does it maybe have to do with InheritedResources?
I believe that if you don't add a constraint to your show route saying that it will only accept digits, everything you put after posts are mapped to be an id to that route.
That means that if your try to access posts/something it would probably throw an ActiveRecord error showing that it couldn't find the Post with id=something.
To add a constraint, add constraints like this:
resources :success_criteria, except: :new, constraints: { id: /\d+/ }

Signout from a controller in rails 4

I want to signout from a controller. My controller looks like
def update
if #attendance.update_attribute(:logout_at, Time.now.localtime)
redirect_to signout_path and return
end
end
And my routes looks like
devise_scope :employees do
get "signout" => "devise/sessions#destroy"
end
devise_for :employees, :controllers => { registrations: 'registrations' }
But It gives error
Unknown action
Could not find devise mapping for path "/signout". This may happen for
two reasons: 1) You forgot to wrap your route inside the scope block.
For example: devise_scope :user do get "/some/route" =>
"some_devise_controller" end 2) You are testing a Devise controller
bypassing the router. If so, you can explicitly tell Devise which
mapping to use: #request.env["devise.mapping"] =
Devise.mappings[:user]
How can I do that? Please Help me out.
Thanks in advance.
You are redirecting, which makes a GET request to devise#sessions#destroy, a route that doesn't exist. The signout route in Devise is a mapped to a DELETE request. Instead of redirecting you should directly call the sign_out method that Devise makes available to you. After that be sure to redirect the user somewhere, maybe the login page.
A side note, in Rails 4 you can call update(attribute: value) directly. You don't need to call return either.
def update
#attendance.update(logout_at: Time.now.localtime)
sign_out
redirect_to login_path
end
I removed the if statement that wrapped the update call. By using one you are implying that there maybe a reason the save will not happen because of validation error, for example, and you need to provide feedback to the user. But in this case it's more likely to be an exception since there is no data input by the user. You can handle that at the application level.

Devise routing: is there a way to remove a route from Rails.application.routes?

devise_for creates routes including a DELETE route, which we want to remove, and devise_for doesn't support an :except or :only option.
How can I remove a route from Rails.application.routes? Either in the draw block, or afterward?
Here are details of a bug, which was the reason we needed to remove the route.
we were issuing a DELETE request to a custom UJS controller action
in the controller action we were removing what we wanted to, then doing a 302 redirect. This was a bad idea, and we have since corrected it by returning some JSON instead.
some clients, upon receiving the 302 would issue a new DELETE request to the redirect, which routes to a Devise delete route! Thereby inadvertantly deleting the person! Yikes. We were assuming this would be a GET. Bad assumption.
This bug has been fixed, but i would like to remove the route nonetheless.
Here is what I did in the end, which was suggested by the bounty-winner in his quote from JoseĀ“ Valim:
In config/routes.rb, I added this above the devise_for call, which sets up the rest of my 'people' routes:
delete '/person', :to => 'people#destroy'
Then in my existing people_controller.rb, I added a no-op method:
def destroy
render :nothing => true
end
I'm still a little irked that there isn't a simple way to just remove the route from the RouteSet. Also, the delete route still exists for the devise controller, but it won't get called because rails looks for the first match in config/routes.rb and returns it.
Here is what Jose Valim (the author of devise) has to say on the subject:
There is no way to remove routes individually. Or you use :skip to
remove all and draw the ones you need manually or you overwrite this
routes by defining a route to the same path first in your config/
routes.rb
So the short answer to your question is no, you can't delete that one route. You can of course try doing things like patching the devise_for method, but that would be a somewhat involved undertaking (a day or several worth of effort). I'd just use the :skip option, then implement the routes you do want for that controller and leave off the one that you don't.
Yes, kinda. You can completely overwrite devise controllers used and write your own (inheriting Devise's if needed). This wiki page can serve as guideline.
Edit
Why I have said kinda :)
Overriding sessions using:
devise_for :users, :controllers => { :sessions => 'custom_devise/sessions'}, :skip => [:sessions] do
get 'sign_in' => 'custom_devise/sessions#new', :as => :new_user_session
post 'sign_in' => 'custom_devise/sessions#create', :as => :user_session
end
will give you only two routes [:get, :post], but not :destroy
new_user_session GET /sign_in(.:format) {:controller=>"custom_devise/sessions", :action=>"new"}
user_session POST /sign_in(.:format) {:controller=>"custom_devise/sessions", :action=>"create"}
So, effectively, you skip destroy/delete route. Now in controller you can go:
class SessionsController < Devise::SessionsController
def new
super
end
def create
super
end
end
You can now repeat the process for registrations, passwords and unlocks.
Second Edit
Ah, yes there is another, simpler way. You can manually create routes (documentation) using devise_scope also known as "as" without overriding:
as :user do
get "sign_in", :to => "devise/sessions#new"
post "sign_in", :to => "devise/sessions#create"
...
end
Gives:
sign_in GET /sign_in(.:format) {:controller=>"devise/sessions", :action=>"new"}
POST /sign_in(.:format) {:controller=>"devise/sessions", :action=>"create"}
Third Edit
Also, you could overwrite part of Devise in charge of creating these routes, (only to be used in applications that will have no devise "destroy" route whatsoever), by creating an initializer:
module ActionDispatch::Routing
extend ActionDispatch::Routing
class Mapper
protected
def devise_session(mapping, controllers) #:nodoc:
resource :session, :only => [], :controller => controllers[:sessions], :path => "" do
get :new, :path => mapping.path_names[:sign_in], :as => "new"
post :create, :path => mapping.path_names[:sign_in]
end
end
def devise_registration(mapping, controllers) #:nodoc:
path_names = {
:new => mapping.path_names[:sign_up],
:cancel => mapping.path_names[:cancel]
}
resource :registration, :only => [:new, :create, :edit, :update], :path => mapping.path_names[:registration],
:path_names => path_names, :controller => controllers[:registrations] do
get :cancel
end
end
end
end
Note that this fix removes all destroy routes used in Devise (there are only two in "sessions" and "registrations") and is a fix only for this specific case.
In addition
You could also add :except option to routes. In order to do it, you must add devise_for method (copy it from original and modify to suit your wishes) to Mapper class so it sends [:except] member of options to above-mentioned (in code) private methods.. Then you should modify those to add routes based on conditions.
Fastest, dirty way, would be to add #scope[:except] = options[:except] and then to modify private methods so that except hash (if you decide to have fine grained route control like: :except => {:sessions => [:destroy]}, thus making :skip obsolete) or array (if you want to remove this specific action from all routes, like: :except => [:destroy]) is checked before adding route.
Anyway, there are plenty ways to achieve what you need. It's up to you to pick the one you think is best suited.
Actually devise_for does support :skip and :only, for example (docs):
devise_for :user, :skip => :registration
This will skip all the registration controller's routes, rather than one specifically. You could then implement the routes you need. This seems cleaner than removing the route after the fact.
UPDATE:
Another possible solution is to use Rails' advanced constraints feature to block the unwanted route completely:
# config/routes.rb
constraints lambda {|req| req.url =~ /users/ && req.delete? ? false : true} do
devise_for :users
end
Here is a post on using lambdas for route constraints. The request object is explained here. This might be the simplest solution.
I found a simple solution with Devise 4.2.0 and Rails 5.0.1. I think this will work with Rails 4, and I'm uncertain about older versions of Devise.
Create an initializer overriding the devise_* route helpers. Examples methods are devise_session, devise_password, devise_confirmation, devise_unlock, and devise_registration. Check out the source.
Ensure the initializer is loaded after the Devise initializer by giving the filename a larger alphanumeric value.
For example, Devise creates a :confirmation route with the :new, :create, and :show actions. I only want the :create action.
# config/initializers/devise_harden.rb
module ActionDispatch::Routing
class Mapper
# Override devise's confirmation route setup, as we want to limit it to :create
def devise_confirmation(mapping, controllers)
resource :confirmation, only: [:create],
path: mapping.path_names[:confirmation], controller: controllers[:confirmations]
end
end
end
Now POST /auth/confirmation is the only route setup for confirmation.

RSpec code example basics with CRUD method calls

After generate rspec:install in a Rails 3 project any new scaffolding will include some default specs. I'm confused about the get, post, put & delete methods and what they're actually being called on?
specifically, in this example, the line delete :destroy, :id => "1" is being called on what exactly? the controller? but the controller doesn't have a 'delete' method...though it does have destroy. but calling 'delete' on it shouldn't do anything so passing :destroy as an argument is meaningless... how does this work?
Here's a portion of the generated specs for a resources_controller. I've left out, but the same thing exists for put :update and post :create and get :edit, :show, :new & :index
#app/controllers/resources_controller.rb
describe ResourcesController do
def mock_resource(stubs={})
#mock_resource ||= mock_model(Resource, stubs).as_null_object
end
...
describe "DELETE destroy" do
it "destroys the requested resource" do
Resource.stub(:find).with("37") { mock_resource }
mock_resource.should_receive(:destroy)
delete :destroy, :id => "37"
end
it "redirects to the resources list" do
Resource.stub(:find) { mock_resource }
delete :destroy, :id => "1"
response.should redirect_to(resources_url)
end
end
end
get, post, put, and delete are the HTTP verbs used in the request. See: http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods
And yes, the following argument is the action being called on your controller, :update, :create, etc.
When you write controller specs, RSpec includes the ControllerExampleGroup module, which "extends ActionController::TestCase::Behavior to work with RSpec.".
ActionController::TestCase::Behavior is where these methods are defined.

Resources