Rails and OAuth controller test - ruby-on-rails-4

I am developing a web-app using Ruby on Rails (4.2.0) and I faced the implementation the login via Oauth (I followed the RailsCast tutorials #235 and #236). In particular for the authentication I implemented the following model
class Authentication < ActiveRecord::Base
# an authentication entry belongs to one user
belongs_to :user
# provider must be present
validates :provider, presence: true
# uid must be present
validates :uid, presence: true
# uniqueness of the couple user_id - provider
validates_uniqueness_of :user_id, :scope => :provider
end
and the associated create action for the authentication controller is
def create
omniauth = request.env["omniauth.auth"]
authentication =Authentication.find_by_provider_and_uid(omniauth['provider'],omniauth['uid'])
if authentication
flash[:success] = "Welcome back #{omniauth['info']['name']}!"
log_in(authentication.user)
redirect_to root_url
elsif current_user
current_user.authentications.create(provider: omniauth['provider'], uid: omniauth['uid'])
flash[:success] = "Welcome #{omniauth['info']['name']}!"
redirect_to root_url
else
user = User.new
user.authentications.build(:provider => omniauth ['provider'], :uid => omniauth['uid'])
if user.save
flash[:success] = "Signed in successfully."
redirect_to root_url
else
session[:omniauth] = omniauth.except('extra')
flash[:info] = "Just one step to go!"
redirect_to signup_url
end
end
end
While route.rb configuration file contains
match '/auth/:provider/callback' => 'authentication#create', via: [:get, :post]
Now I get stucked in writing down the controller test for two main reasons:
How can I make the post in test? With the following test unit
class AuthenticationControllerTest < ActionController::TestCase
test "post request" do
post :create
end
end
I get the following error
ActionController::UrlGenerationError: ActionController::UrlGenerationError: No route matches {:action=>"create", :controller=>"authentication"}
test/controllers/authentication_controller_test.rb:6:in `block in <class:AuthenticationControllerTest>'
test/controllers/authentication_controller_test.rb:6:in `block in <class:AuthenticationControllerTest>'
How can I create a "fake" env variable for the test?
Surfing the web I was able to find only tutorials using Capybara or Cucumber and nothing with the standard Rails tests.
Any kind of help will be really appreciated!
Andrea

Related

rails NoMethodError: undefined method `helper_method' for ApplicationHelper:Module

I have followed this tutorial and its working correctly.
[updated after below answer]
I have moved the code to the Application Controller (previously defined as a helper) to determine if the current user is logged in
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
helper_method :current_user
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
end
I have created a module to create a client object and make an api call, this functionality may be used by one or more objects so it seemed like a good idea to create it as a module instead of a controller.
require 'base64'
require 'rubygems'
require 'json'
require 'google/api_client'
require 'google/api_client/client_secrets'
require 'net/https'
require 'uri'
module GoogleClient
include ApplicationController
PLUS_LOGIN_SCOPE = 'https://www.googleapis.com/auth/plus.login'
# Build the global client
$credentials = Google::APIClient::ClientSecrets.load("#{Rails.root}/config/client_secret_xxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com.json")
$authorization = Signet::OAuth2::Client.new(
:authorization_uri => $credentials.authorization_uri,
:token_credential_uri => $credentials.token_credential_uri,
:client_id => $credentials.client_id,
:client_secret => $credentials.client_secret,
:redirect_uri => $credentials.redirect_uris.first,
:scope => PLUS_LOGIN_SCOPE)
$client = Google::APIClient.new(options = {:application_name => 'xxx-xxx-xxx'} )
def GoogleClient.get_people
if current_user
# Authorize the client and construct a Google+ service.
$client.authorization.update_token!(current_user.oauth_token.to_hash)
plus = $client.discovered_api('plus', 'v1')
# Get the list of people as JSON and return it.
response = $client.execute!(plus.people.list,
:collection => 'visible',
:userId => 'me').body
content_type :json
puts response
else
redirect_to root_url
end
end
end
the user model is;
require 'google_client'
class User < ActiveRecord::Base
include GoogleClient
# after_save GoogleClient.connect
def self.from_omniauth(auth)
where(provider: auth["provider"], uid: auth["uid"]).first_or_initialize.tap do |user|
user.provider = auth.provider
user.uid = auth.uid
user.name = auth.info.name
user.email = auth.info.email
user.oauth_token = auth.credentials.token
user.oauth_expires_at = Time.at(auth.credentials.expires_at)
user.save!
end
testing it in the console results in
2.1.0 :001 > GoogleClient.get_people
NoMethodError: undefined method `helper_method' for ApplicationHelper:Module
Is it possible to call to a helper method in a module? How should I implement this code if a module is incorrect
** update Correct module code but the api request has a redirect uri error ** explained here in this post
"Notice that modules in /lib are not automatically loaded. Instead, you will need to add this line in your config/application.rb file file config block :"
config.autoload_paths += %W(#{config.root}/lib)
User Model
class User < ActiveRecord::Base
include Google::GoogleClient
def self.from_omniauth(auth)
where(provider: auth["provider"], uid: auth["uid"]).first_or_initialize.tap do |user|
user.provider = auth.provider
user.uid = auth.uid
user.name = auth.info.name
user.email = auth.info.email
user.oauth_token = auth.credentials.token
user.oauth_expires_at = Time.at(auth.credentials.expires_at)
user.save!
end
end
if current_user
self.get_people()
else
redirect_to root_url
end
end
'lib/google/google_client.rb'
require 'google/api_client'
require 'google/api_client/client_secrets'
require 'google/api_client/auth/installed_app'
module Google
module GoogleClient
# Initialize the client.
client = Google::APIClient.new(
:application_name => 'xxx-xxx',
:application_version => '1.0.0'
)
# Initialize Google+ API. Note this will make a request to the
# discovery service every time, so be sure to use serialization
# in your production code. Check the samples for more details.
# Load client secrets from your client_secrets.json.
client_secrets = Google::APIClient::ClientSecrets.load("#{Rails.root}/config/client_secret_XXXXXXXXXXXXXXXXXXXXXXXx.apps.googleusercontent.com.json")
# Run installed application flow. Check the samples for a more
# complete example that saves the credentials between runs.
flow = Google::APIClient::InstalledAppFlow.new(
:client_id => client_secrets.client_id,
:client_secret => client_secrets.client_secret,
:scope => ['https://www.googleapis.com/auth/plus.me']
)
client.authorization = flow.authorize
def get_people
# Make an API call.
result = client.execute(
:api_method => plus.activities.list,
:parameters => {'collection' => 'public', 'userId' => 'me'}
)
puts result.data
end
end
end
Move both of these to ApplicationController:
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
That will give you a current_user method that works in controllers, helpers, and views.

Devise::InvitationsController reports Unpermitted parameters

I have a problem with devise_invitable 1.4.0 and strong parameters when I add additional custom parameters and I really hope somebody can guide me in the right direction. I am able to send invitations, but when an invited user accepts an invitation and enters a desired username, maiden name, password and confirmed password, the following error is shown:
Processing by Users::InvitationsController#update as HTML
Unpermitted parameters: username, name
The user is created as expected, but the 'username' and 'name' columns in the database are empty.
I have tried all the suggestions I could find for related issues, but none of the worked. I have noticed that if I change the app/controllers/users/invitations_controller.rb file in any way (eg inserting a blank space on an empty line) without restarting the webserver (Thin) the problem disappears - but the problem reappears when the webserver is restarted.
The various relevant files look like this:
routes.rb:
Rails.application.routes.draw do
root to: 'visitors#index'
#Tell rails to use the Devise controllers that were generated with this command:
# > rails generate devise:controllers users
#Using these generated controllers allows us to overwrite anything in the deault controllers.
devise_for :users, :path_names => {:sign_in => 'login', :sign_out => 'logout'}, controllers: {confirmations: "users/confirmations", passwords: "users/passwords", registrations: "users/registrations", sessions: "users/sessions", unlocks: "users/unlocks", :invitations => 'users/invitations'}
resources :users
end
config/initializers/devise.rb
Devise.setup do |config|
...
...
config.scoped_views = true
config.authentication_keys = [ :username ]
...
...
end
app/controllers/users/invitations_controller.rb:
class Users::InvitationsController < Devise::InvitationsController
private
# this is called when creating invitation
# should return an instance of resource class
def invite_resource
## skip sending emails on invite
resource_class.invite!(invite_params, current_inviter) do |u|
u.tenant = current_inviter.tenant
u.role = :user
end
end
def after_invite_path_for(resource)
users_path
end
def resource_params
params.permit(user: [:name, :email,:invitation_token, :username])[:user]
end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
around_filter :scope_current_tenant
before_filter :configure_permitted_parameters, if: :devise_controller?
if Rails.env.development?
# https://github.com/RailsApps/rails-devise-pundit/issues/10
include Pundit
# https://github.com/elabs/pundit#ensuring-policies-are-used
# after_action :verify_authorized, except: :index
# after_action :verify_policy_scoped, only: :index
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
end
#############################################################################
private
#############################################################################
if Rails.env.development?
def user_not_authorized
flash[:alert] = "Access denied." # TODO: make sure this isn't hard coded English.
redirect_to (request.referrer || root_path) # Send them back to them page they came from, or to the root page.
end
end
def current_tenant
#current_tenant ||= current_user.tenant unless current_user.nil?
end
helper_method :current_tenant
def scope_current_tenant(&block)
if current_tenant.nil?
scope_visitor_schema
yield
else
current_tenant.scope_schema("public", &block)
end
end
def scope_visitor_schema()
original_search_path = ActiveRecord::Base.connection.schema_search_path
ActiveRecord::Base.connection.schema_search_path = 'public'
ensure
ActiveRecord::Base.connection.schema_search_path = original_search_path
end
#############################################################################
protected
#############################################################################
def configure_permitted_parameters
# Only add some parameters
devise_parameter_sanitizer.for(:account_update).concat [:name, :email]
# Override accepted parameters
devise_parameter_sanitizer.for(:accept_invitation) do |u|
u.permit(:name, :username, :password, :password_confirmation,
:invitation_token)
end
end
end
app/models/user.rb:
class User < ActiveRecord::Base
enum role: [:user, :admin]
after_initialize :create_tenant, :if => :new_record?
belongs_to :tenant
# has_many :invitations, :class_name => self.to_s, :as => :invited_by
scope :unconfirmed, -> { where(confirmed_at: nil) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
# validate :username, presence: true, uniqueness: true, format: { with: /[a-zA-Z0-9]{4,20}/ }
def displayed_username
username.nil? ? "N/A" : username
end
def displayed_name
name.nil? ? "N/A" : name.titleize
end
def create_tenant
#The create_tenant method will also be called when looking up a user,
#so the following ensures a tenant is only created if it does not already
#exist - and the user has not been invited and assigned to an existing tenant:
if self.tenant.nil?
#Set role to 'admin' if a tenant is about to be created:
self.role = :admin #if self.tenant.nil?
self.tenant = Tenant.new
end
end
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :invitable, :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable
end
I finally found a fix, which was to place the parameter sanitizer directly in users/invitations_controller.rb instead of the application_controller.rb.
class Users::InvitationsController < Devise::InvitationsController
before_filter :configure_permitted_parameters, if: :devise_controller?
private
def configure_permitted_parameters
devise_parameter_sanitizer.for(:accept_invitation) do |u|
u.permit(:username, :name, :email, :password, :password_confirmation, :invitation_token)
end
end
end

Testing custom devise registration controller update action with Rspec

I followed this devise wiki documentation on how to write a custom update action for the registration controller when you want to allow users to edit their account without providing their passwords except if changing their passwords themselves.
Devise Wiki - How to Allow Users to Edit Account Without Providing a Password.
However, I can't figure out what's missing in my Rspec test to make it pass. Here are the relevant code snippets:
app/controllers/registrations_controller.rb
def update
#user = User.find(current_user.id)
successfully_updated = if needs_password?(#user, params)
#user.update_with_password(devise_parameter_sanitizer.sanitize(:account_update))
else
# remove the virtual current_password attribute
# update_without_password doesn't know how to ignore it
params[:user].delete(:current_password)
#user.update_without_password(devise_parameter_sanitizer.sanitize(:account_update))
end
if successfully_updated
set_flash_message :notice, :updated
# Sign in the user bypassing validation in case their password changed
sign_in #user, :bypass => true
redirect_to users_path
else
render "edit"
end
end
spec/factories/users.rb
FactoryGirl.define do
factory :user do
email { Faker::Internet.email }
password 'XXXXXXXXX'
first_name { Faker::Name.first_name }
middle_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
end
end
spec/controllers/registrations_controller_spec.rb
describe "PUT #update" do
login_pcp
let(:user) { FactoryGirl.create(:user, first_name: 'Tom') }
it "changes user attributes" do
attrs = FactoryGirl.attributes_for(:user, first_name: 'Jerry')
attrs.delete(:password)
put :update, user: attrs
user.reload
assigns[:user].should_not be_new_record
expect(user.first_name).to eq 'Jerry'
expect(flash[:notice]).to eq 'You updated your account successfully.'
end
end
When I run the spec I get the following error:
Failures:
1) RegistrationsController PUT #update changes user attributes
Failure/Error: expect(user.first_name).to eq 'Jerry'
expected: "Jerry"
got: "Tom"
(compared using ==)
# ./spec/controllers/registrations_controller_spec.rb:55:in `block (3 levels) in <top (required)>'
For some reason, it's not saving the update. I'm not sure if a password should be entered in order for the update to take place? Any help would be appreciated. Thanks!
The test now looks like this and it passes:
describe "PUT #update" do
before :each do
#request.env['devise.mapping'] = Devise.mappings[:user]
user_tom = FactoryGirl.create(:user, email: 'tom#test.com')
sign_in user_tom
end
it "changes user attributes" do
put :update, user: { email: 'jerry#test.com' }
subject.current_user.reload
assigns[:user].should_not be_new_record
expect(subject.current_user.email).to eq 'jerry#test.com'
expect(flash[:notice]).to eq 'You updated your account successfully.'
end
end
I ran into this issue as well, but as I can see it's because when you fill the update form, you will be required to fill in a field called "Current password". Since the data won't be updated unless you fill in the filed. When you use factory girl to produce user data, there is no this value. I solved it as can be seen in following code.
describe "PATCH #UPDATE" do
before :each do
#user = create(:user)
#old_email = #user.email
sign_in #user
end
context 'valid attributes' do
it "updates user attributes" do
patch :update, id: #user,
user: attributes_for(:user, current_password: "password")
expect(#user.reload.email).not_to eq(#old_email)
end
end
end

device facebook oAuth stucked on redirect page

Im using devise omniauthable for user authentication both with google and facebook. Google works just fine but facebook gets stucked on a redirect page, although on their platform the user logs in correctly (on facebook).
This is the log i get:
2014-09-01T15:26:41.996884+00:00 app[web.1]: (facebook) Request phase initiated.
2014-09-01T15:26:42.211524+00:00 app[web.1]: (facebook) Request phase initiated.
2014-09-01T15:26:41.994627+00:00 app[web.1]: Started GET "/users/auth/facebook?locale=es" for 190.15.201.45 at 2014-09-01 15:26:41 +0000
2014-09-01T15:26:42.205674+00:00 app[web.1]: Started GET "/users/auth/facebook?locale=es" for 190.15.201.45 at 2014-09-01 15:26:42 +0000
2014-09-01T15:26:42.217355+00:00 heroku[router]: at=info method=GET path="/users/auth/facebook?locale=es" host=myapp.herokuapp.com request_id=2b9aab45-c511-4ac9-b36f-4e6925cba3aa fwd="190.15.201.45" dyno=web.1 connect=2ms service=16ms status=302 bytes=1284
My routes:
devise_for :users, :controllers => { omniauth_callbacks: "users/omniauth_callbacks" }
My omniauth_callbacks_controller:
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def self.provides_callback_for(provider)
class_eval %Q{
def #{provider}
#user = User.find_for_oauth(env["omniauth.auth"], current_user)
if #user.persisted?
sign_in_and_redirect #user, event: :authentication
set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format?
else
session["devise.#{provider}_data"] = env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
}
end
[:google_oauth2, :facebook].each do |provider|
provides_callback_for provider
end
My user model:
class User < ActiveRecord::Base
devise :database_authenticatable, :confirmable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :omniauthable
belongs_to :company
before_save :create_user_company
def create_user_company
if self.company_id.nil?
company = Company.new
company.email = self.email
company.save
self.company_id = company.id
self.admin = true
end
end
def self.find_for_oauth(auth, signed_in_resource = nil)
identity = Identity.find_for_oauth(auth)
user = signed_in_resource ? signed_in_resource : identity.user
if user.nil?
email = auth.info.email
user = User.where(:email => email).first if email
if user.nil?
user = User.new(
name: auth.extra.raw_info.name,
email: email ? email : "temp_email#mail.com",
password: Devise.friendly_token[0,20]
)
user.skip_confirmation!
user.save!
end
end
if identity.user != user
identity.user = user
identity.save!
end
user
end
end
Devise initializer is the basic:
config.omniauth :facebook, ENV['FB_APP_ID'], ENV['FB_APP_SECRET']
But i also tried this (didnt work):
config.omniauth :facebook, ENV['FB_APP_ID'], ENV['FB_APP_SECRET'],{client_options: {ssl: {ca_file: Rails.root.join('lib/assets/cacert.pem').to_s}}}
And im using this gems:
gem 'omniauth'
gem 'omniauth-google-oauth2'
gem 'omniauth-facebook'
gem 'devise'
gem 'figaro' #for safe saving of env vars
I would appreciate any hint you might have.
Thanks in advance.
PS: this is not the answer
Ok, if anyone encounters the same problem, this fixed it:
Go into your facebook developer console (developers.facebook)
Enter your App:
On Setting you need to add a contact email.
On Status and Reviews you will find this question (former Sandbox): "Do you want to make this app and all its live features available to the general public?".
Click "Yes".

Why is my enum value wrong in minitest using Pundit gem?

I created a starter app from RailsApps with the rails-devise-pundit example app. I am trying to write a user controller test because I plan to change some functionality and I want to make sure things still work. The pundit UserPolicy is not returning the correct value which is based on a role enum in the User class. The UserPolicy.index? method seen below is returning false when called from the first test in UsersControllerTest. Sorry there is a lot of code and detail here. I hope everyone can follow it.
Here's the failing test in UsersControllersTest. The response is a :redirect instead of :success.
require "test_helper"
class UsersControllerTest < ActionController::TestCase
def setup
#admin = users(:admin)
#admin.role = :admin
end
test "should get index page when authenticated as an admin" do
sign_in #admin
get :index
assert_response :success
end
...
end
Here's my user controller class just showing the index method where my problem is. authorize #users should call the UserPolicy.index? method.
class UsersController < ApplicationController
before_filter :authenticate_user!
after_action :verify_authorized, except: [:show]
def index
#users = User.all
authorize #users
end
...
end
My pundit user policy class. When I change the index? method so it returns true, the response in my UsersControllerTest is :success. So for some reason #user.admin? is not returning the correct value.
class UserPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
def index?
#user.admin?
end
...
end
What is even stranger is that I created a UserPolicyTest class and when I test calling index? from there, I get the correct response. This test works correctly:
require 'test_helper'
class UserPolicyTest < ActiveSupport::TestCase
def setup
#admin = users(:admin)
#admin.role = :admin
end
def test_index
policy = UserPolicy.new #admin, nil
assert policy.index?
end
end
Here is my User model:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable
enum role: [:user, :vip, :admin]
after_initialize :set_default_role, :if => :new_record?
validates :name, presence: true
def set_default_role
self.role ||= :user
end
end
Here's my test fixture for an admin user:
admin:
email: admin#example.com
name: Mr Admin
role: admin
encrypted_password: $2a$10$PoBe1MvkoGJsjMVTEjKqgeBUp.xdfzWoiDjBzQhtLAj16NqIa2fOy
remember_created_at: nil
sign_in_count: 3
current_sign_in_at: 2014-01-02 08:31:23
last_sign_in_at: 2014-01-02 08:31:23
current_sign_in_ip: 127.0.0.1
last_sign_in_ip: 127.0.0.1
confirmation_token: nil
confirmed_at: 2014-01-02 08:31:23
confirmation_sent_at: 2014-01-02 08:30:59
created_at: 2014-01-02 08:30:59
updated_at: 2014-01-02 08:31:23
I found that setting the role in the fixture doesn't work. I'm guessing that's because of the after_initialize :set_default_role, :if => :new_record? line in my User model. If there's another reason or a better way to handle this, please let me know.
UPDATE: Maybe this is being caused by strong parameters. When I tried debugging my code with pry, I found that in the UsersControllerTest, after signing in, the admin user had a role of 2 which is correct. But when it got to User.Policy.index?, the role was 0. I may need to add the role field to the devise strong parameters. I saw something about how to do that a while back. It didn't look easy. If someone knows the answer before I get to it, please let me know.
After I changed the value of #admin.role in setup, I didn't save the user. After adding #admin.save to the setup method, the test passed.