James B. Byrne
James B. Byrne

Reputation: 1064

Devise Logout - Cucumber step fails but application runs correctly

Ruby-2.2.0 Rails-4.2 Devise-3.4.1 Declarative_Authorization-0.5.7

I am converting a Rails-3.2 app to Rails-4. The app employs both Devise authentication and Declarative Authorization. I am using the default Devise controller.

At this juncture I have most of the authentication scenarios passing. What I have run into however is that I cannot logout through Cucumber but I can in the browser. The step in question looks like this:

 When /select end the current session/ do
  selector_type = "#"
  selector_value = link = "session_end_action_id"
  selector = selector_type + selector_value
  click_button( selector_value )
end

The routes look like this:

. . .
                  new_user_session GET    /users/sign_in(.:format)                                        devise/sessions#new
                      user_session POST   /users/sign_in(.:format)                                        devise/sessions#create
              destroy_user_session DELETE /users/sign_out(.:format)                                       devise/sessions#destroy
                     user_password POST   /users/password(.:format)                                       devise/passwords#create
                 new_user_password GET    /users/password/new(.:format)                                   devise/passwords#new
                edit_user_password GET    /users/password/edit(.:format)                                  devise/passwords#edit
                                   PATCH  /users/password(.:format)                                       devise/passwords#update
                                   PUT    /users/password(.:format)                                       devise/passwords#update
                       user_unlock POST   /users/unlock(.:format)                                         devise/unlocks#create
                   new_user_unlock GET    /users/unlock/new(.:format)                                     devise/unlocks#new
                                   GET    /users/unlock(.:format)                                         devise/unlocks#show
                           account POST   /account(.:format)                                              users#create
                       new_account GET    /account/new(.:format)                                          users#new
                      edit_account GET    /account/edit(.:format)                                         users#edit
                                   GET    /account(.:format)                                              users#show
                                   PATCH  /account(.:format)                                              users#update
                                   PUT    /account(.:format)                                              users#update
                                   DELETE /account(.:format)                                              users#destroy
                      authenticate GET    /authenticate(.:format)                                         devise/sessions#new

. . .

                    users#index
                                   POST   /users(.:format)                                                users#create
                          new_user GET    /users/new(.:format)                                            users#new
                         edit_user GET    /users/:id/edit(.:format)                                       users#edit
                              user GET    /users/:id(.:format)                                            users#show
                                   PATCH  /users/:id(.:format)                                            users#update
                                   PUT    /users/:id(.:format)                                            users#update
                                   DELETE /users/:id(.:format)                                            users#destroy

. . .

The code in the view looks like this:

  <%-if current_user-%>
  <span class="authenticated_session" id="authenticated_session">
  <%=button_to( I18n.t( :session_end ).strip.titleize, 
      :destroy_user_session, 
      :class => "button logout",
      :confirm => I18n.t( :session_end_confirm ).strip.titleize,
      :id => :session_end_action_id,
      :method => :delete, 
      :title => I18n.t( :session_end_logout ).strip.titleize )-%>
  </span
<%-else-%>
  <%=button_to( I18n.t( :session_start ).strip.titleize, 
      :new_user_session, 
      :class => "button login",
      :id => :session_start_action_id_top, 
      :method => :get, 
      :title => I18n.t( :session_start_login ).strip.titleize )-%>
<%-end-%>

When I am authenticated and press the logout button then I am logged out. When I run the cucumber step I get an authorisation error from Declarative Authorization.

  <p class='security classified' style='color: orangered'>
      you are not authorised to access the requested resource</p>    
  <br/>
  <!-- End of header section from layouts/application.html.erb -->

This is generated here in the application controller:

def permission_denied
  if current_user
    flash[:security_classified] =  I18n.t( :security_classified ).strip
  else
    flash[:security_restricted] =  I18n.t( :security_restricted ).strip
  end
  redirect_back_or_default( welcome_url )
end

and that is apparently being called from the Users controller:

  User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ?  ORDER BY "users"."id" ASC LIMIT 1  [["id", 21]]

  Rendered public/hll_authorisation_notice.html (0.1ms)

  Rendered welcome/show.html.erb within layouts/application (3.6ms)
Completed 200 OK in 105ms (Views: 100.4ms | ActiveRecord: 0.6ms)

Started DELETE "/users/sign_out" for 127.0.0.1 at 2015-01-26 15:19:48 -0500

Processing by UsersController#destroy as HTML
  Parameters: {"id"=>"sign_out"}
   (0.2ms)  SELECT COUNT(*) FROM "users"
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ?  ORDER BY "users"."id" ASC LIMIT 1  [["id", 21]]
  CACHE (0.0ms)  SELECT COUNT(*) FROM "users"
  Role Load (0.4ms)  SELECT "roles".* FROM "roles" INNER JOIN "clearances" ON "roles"."id" = "clearances"."role_id" WHERE "clearances"."user_id" = ?  [["user_id", 21]]
  CACHE (0.0ms)  SELECT COUNT(*) FROM "users"

Redirected to http://www.example.com/welcome
Filter chain halted as :filter_access_filter rendered or redirected
Completed 302 Found in 23ms (ActiveRecord: 1.0ms)

Started GET "/welcome" for 127.0.0.1 at 2015-01-26 15:19:48 -0500
Processing by WelcomeController#show as HTML
   (0.2ms)  SELECT COUNT(*) FROM "users"
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ?  ORDER BY "users"."id" ASC LIMIT 1  [["id", 21]]

  Rendered public/hll_authorisation_notice.html (0.1ms)

  Rendered welcome/show.html.erb within layouts/application (1.5ms)
Completed 200 OK in 61ms (Views: 57.2ms | ActiveRecord: 0.4ms)
   (0.4ms)  rollback transaction

Inspecting (reformatted for this purpose) the object in the permission_denied method reveals this:

    #<UsersController:0x00000006ba52d8 @_action_has_layout=true,
 @_routes=nil, @_headers={"Content-Type"=>"text/html"}, @_status=200,
 @_request=#<ActionDispatch::Request:0x00000006ba51c0
 @env={"rack.version"=>[1, 3], "rack.input"=>#<StringIO:0x00000006c378e0>,
 "rack.errors"=>#<StringIO:0x00000006c379a8>, "rack.multithread"=>false,
 "rack.multiprocess"=>true, "rack.run_once"=>false,
 "REQUEST_METHOD"=>"DELETE", "SERVER_NAME"=>"www.example.com",
 "SERVER_PORT"=>"80", "QUERY_STRING"=>"", "PATH_INFO"=>"/users/sign_out",
 "rack.url_scheme"=>"http", "HTTPS"=>"off", "SCRIPT_NAME"=>"",
 "CONTENT_LENGTH"=>"14", "rack.test"=>true, "REMOTE_ADDR"=>"127.0.0.1",
 "HTTP_REFERER"=>"http://www.example.com/",
 "HTTP_HOST"=>"www.example.com",
 "CONTENT_TYPE"=>"application/x-www-form-urlencoded",
 "HTTP_COOKIE"=>"_proforma_session=. . .

So, this is definitely an issue with the Users Controller. But error is only encountered during a Cucumber run. When I serve the application with Rails server and access it from a browser, sign in and sign off then I am logged out without any error as shown in log extract below.

Started DELETE "/users/sign_out" for ::1 at 2015-01-26 15:45:50 -0500
Processing by Devise::SessionsController#destroy as HTML
  Parameters: {"authenticity_token"=>"Xl9Ui1a6jt8gyjZOuh0lsefUqFI1eEunaXivaEdfwWhMofYhYbRumnZlsRQjwmjWiC1C7sI7O3FwDgEf9lJzJw=="}
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ?  ORDER BY "users"."id" ASC LIMIT 1  [["id", 1]]
   (0.2ms)  SELECT COUNT(*) FROM "users"
   (0.1ms)  begin transaction
   (0.4ms)  UPDATE "users" SET "accessed_at" = '2015-01-26 20:45:50.179843', "changed_at" = '2015-01-26 20:45:50.180748', "lock_version" = 20 WHERE ("users"."id" = 1 AND "users"."lock_version" = 19)
   (103.2ms)  commit transaction
Redirected to http://localhost:3000/

Has anyone any ideas as to what might be happening here? Is the token value found in the browser sign_out significant?

P.S. If I just do this:

visit('/users/sign_out')

then the step passes. I am taking a wild guess here but is there anything about the javscript used with html buttons in RoR that might be causing what I am seeing?

Upvotes: 0

Views: 369

Answers (1)

James B. Byrne
James B. Byrne

Reputation: 1064

I think that I have discovered what was wrong; accidentally whilst researching a different and unrelated problem.

The issue I believe is the default testing method used by Capybara:

https://github.com/jnicklas/capybara#selecting-the-driver

By default, Capybara uses the :rack_test driver, which is fast but limited: it does not support JavaScript, nor is it able to access HTTP resources outside of your Rack application, such as remote APIs and OAuth services. To get around these limitations, you can set up a different default driver for your features. For example if you'd prefer to run everything in Selenium, you could do:

Capybara.default_driver = :selenium

However, if you are using RSpec or Cucumber, you may instead want to consider leaving the faster :rack_test as the default_driver, and marking only those tests that require a JavaScript-capable driver using :js => true or @javascript, respectively. By default, JavaScript tests are run using the :selenium driver. You can change this by setting Capybara.javascript_driver.

You can also change the driver temporarily (typically in the Before/setup and After/teardown blocks):

Capybara.current_driver = :webkit # temporarily select different driver ... tests ... Capybara.use_default_driver # switch back to default driver

Note: switching the driver creates a new session, so you may not be able to switch in the middle of a test.

Upvotes: 0

Related Questions