I am trying make my tests for user authentication pass when the user sends verification code to the Github/Google service to receive a valid token using Octokit gem. I am running at NoMethodError undefined method `access_token' for nil:NilClass in RSpec.
This is screenshot of my terminal:
undefined method `access_token' for nil:NilClass in RSpeca terminal screenshot of the problem
This is my UserAuthenticator lib:
class UserAuthenticator
class AuthenticationError < StandardError; end
attr_reader :user, :access_token
def initialize(code)
#code = code
end
def perform
raise AuthenticationError if code.blank?
raise AuthenticationError if token.try(:error).present?
prepare_user
#access_token = if user.access_token.present?
user.access_token
else
user.create_access_token
end
end
# ----------------------------------------------------------------------------
private
def client
#client ||= Octokit::Client.new(
client_id: ENV['GITHUB_CLIENT_ID'],
client_secret: ENV['GITHUB_CLIENT_SECRET']
)
end
def token
#token ||= client.exchange_code_for_token(code)
end
def user_data
#user_data ||= Octokit::Client.new(
access_token: token
).user.to_h.slice(:login, :url, :avatar_url, :name)
end
def prepare_user
if User.exists?(login: user_data[:login])
#user = User.find_by(login: user_data[:login])
else
User.create(user_data.merge(provider: 'github'))
end
end
attr_reader :code
end
This is my RSpec file for User authentication:
require 'rails_helper'
RSpec.describe UserAuthenticator do
describe '#perform' do
let(:authenticator) { described_class.new('sample code') }
subject { authenticator.perform }
context 'then the code is invalid' do
let(:error) {
double("Sawyer::Resource", error:'bad_verification_code')
}
before do
allow_any_instance_of(Octokit::Client).to receive(
:exchange_code_for_token).and_return(error)
end
it "should raise an error" do
expect{subject}.to raise_error(UserAuthenticator::AuthenticationError)
expect(authenticator.user).to be_nil
end
end
context 'when code is correct' do
let(:user_data) do
{
login: 'nklobuc1',
url: 'http://example.com',
avatar_url: 'http://example.com/avatar',
name: 'Nikola'
}
end
before do
allow_any_instance_of(Octokit::Client).to receive(
:exchange_code_for_token).and_return('validaccestoken')
allow_any_instance_of(Octokit::Client).to receive(
:user).and_return(user_data)
end
it "should save the user when does not exist" do
expect{subject}.to change{ User.count }.by(1)
expect(User.last.name).to eq('Nikola')
end
it "should reuse already registered user" do
user = create :user, user_data
expect{subject}.not_to change{User.count}
expect(authenticator.user).to eq(user)
end
it "should create and set user's access token" do
pp subject
expect{subject}.to change{AccessToken.count}.by(1)
expect(authenticator.access_token).to be_present
end
end
end
end
This is the User model:
class User < ApplicationRecord
validates :login, presence: true, uniqueness: true
validates :provider, presence: true
has_one :access_token, dependent: :destroy
end
and finally, this is AccessToken model:
class AccessToken < ApplicationRecord
belongs_to :user, class_name: "User", :foreign_key => :user_id
validates_uniqueness_of :user_id
after_initialize :generate_token
private
def generate_token
loop do
break if token.present? && !AccessToken.exists?(token: token)
self.token = SecureRandom.hex(10)
end
end
end
I found an answer. The prepare_user returned nil when I tested
it "should save the user when does not exist" do
expect{subject}.to change{ User.count }.by(1)
expect(User.last.name).to eq('Nikola')
end
The prepare_user just needed a following tweak (another instance variable under else):
def prepare_user
if User.exists?(login: user_data[:login])
#user = User.find_by(login: user_data[:login])
else
#user = User.create(user_data.merge(provider: 'github'))
end
end
Related
I have a customer model that belongs to user, and my controller test for post#create succeeds. But I have a subscription model that belongs to both user and plan, and it is failing (I'm using rails 5.1.2).
Here's my spec:
#rspec/controllers/checkout/subscriptions_controller_spec.rb
require 'rails_helper'
RSpec.describe Checkout::SubscriptionsController, type: :controller do
describe 'POST #create' do
let!(:user) { FactoryGirl.create(:user) }
before do
sign_in user
end
context 'with valid attributes' do
it 'creates a new subscription' do
expect { post :create, params: { subscription: FactoryGirl.attributes_for(:subscription) } }.to change(Subscription, :count).by(1)
end
end
end
end
Subscription controller:
# app/controllers/checkout/subscriptions_controller.rb
module Checkout
class SubscriptionsController < Checkout::CheckoutController
before_action :set_subscription, only: %i[edit update destroy]
before_action :set_options
def create
#subscription = Subscription.new(subscription_params)
#subscription.user_id = current_user.id
if #subscription.valid?
respond_to do |format|
if #subscription.save
# some code, excluded for brevity
end
end
else
respond_to do |format|
format.html { render :new }
format.json { render json: #subscription.errors, status: :unprocessable_entity }
end
end
end
private
def set_subscription
#subscription = Subscription.find(params[:id])
end
def set_options
#categories = Category.where(active: true)
#plans = Plan.where(active: true)
end
def subscription_params
params.require(:subscription).permit(:user_id, :plan_id, :first_name, :last_name, :address, :address_2, :city, :state, :postal_code, :email, :price)
end
end
end
Subscription model -
# app/models/subscription.rb
class Subscription < ApplicationRecord
belongs_to :user
belongs_to :plan
has_many :shipments
validates :first_name, :last_name, :address, :city, :state, :postal_code, :plan_id, presence: true
before_create :set_price
before_update :set_price
before_create :set_dates
before_update :set_dates
def set_dates
# some code, excluded for brevity
end
def set_price
# some code, excluded for brevity
end
end
I'm also using some FactoryGirl factories for my models.
# spec/factories/subscriptions.rb
FactoryGirl.define do
factory :subscription do
first_name Faker::Name.first_name
last_name Faker::Name.last_name
address Faker::Address.street_address
city Faker::Address.city
state Faker::Address.state_abbr
postal_code Faker::Address.zip
plan
user
end
end
# spec/factories/plans.rb
FactoryGirl.define do
factory :plan do
name 'Nine Month Plan'
description 'Nine Month Plan description'
price 225.00
active true
starts_on Date.new(2017, 9, 1)
expires_on Date.new(2018, 5, 15)
monthly_duration 9
prep_days_required 5
category
end
end
# spec/factories/user.rb
FactoryGirl.define do
factory :user do
name Faker::Name.name
email Faker::Internet.email
password 'Abcdef10'
end
end
When I look at the log, I notice that user and plan aren't being populated when running the spec and creating the subscription, which must be why it's failing, since plan is required. But I can't figure out how to fix this. Any ideas? Thanks in advance.
The issue is that, by your model definition, you can only create a Subscription that is associated to an existing Plan:
class Subscription < ApplicationRecord
belongs_to :plan
validates :plan_id, presence: true
end
You could have debugged this issue by either setting a breakpoint in the rspec test and inspecting the response.body; or similarly instead by setting a breakpoint in SubscriptionsController#create and inspecting #subscription.errors. Either way, you should see the error that plan_id is not present (so therefore the #subscription did not save).
The issue stems from the fact that FactoryGirl#attributes_for does not include associated model IDs. (This issue has actually been raised many times in the project, and discussed at length.)
You could just explicitly pass a plan_id in the request payload of your test, to make it pass:
it 'creates a new subscription' do
expect do
post(
:create,
params: {
subscription: FactoryGirl.attributes_for(:subscription).merge(post_id: 123)
}
end.to change(Subscription, :count).by(1)
end
However, this solution is somewhat arduous and error prone. A more generic alternative I would suggest is define the following spec helper method:
def build_attributes(*args)
FactoryGirl.build(*args).attributes.delete_if do |k, v|
["id", "created_at", "updated_at"].include?(k)
end
end
This utilises the fact that build(:subscription).attributes does include foreign keys, as it references the associations.
You could then write the test as follows:
it 'creates a new subscription' do
expect do
post(
:create,
params: {
subscription: build_attributes(:subscription)
}
)
end.to change(Subscription, :count).by(1)
end
Note that this test is still slightly unrealistic, since the Post does not actually exist in the database! For now, this may be fine. But in the future, you may find that the SubscriptionController#create action actually needs to look up the associated Post as part of the logic.
In this case, you'd need to explicitly create the Post in your test:
let!(:post) { create :post }
let(:subscription) { build :subscription, post: post }
...And then send the subscription.attributes to the controller.
Model:
module V1
class Player < ActiveRecord::Base
validates :name, presence: true
validates :default_pull_rate, numericality: true, allow_nil: false
has_many :player_links
end
end
Spec (I even tried explicitly setting the default_pull_rate inline as seen below):
it "creates a new player" do
expect { post :create, format: :json, player: FactoryGirl.attributes_for(:player, default_pull_rate: 5) }.to change(V1::Player, :count).by(1)
end
Factory:
FactoryGirl.define do
factory :player, class: V1::Player do
name "Frank"
default_pull_rate 100
end
Controller:
....
def create
#player = Player.new(player_params)
if #player.save!
redirect_to #player
end
end
private
def player_params
params.require(:player).permit(:name, :default_pull_rated)
end
Error message:
ActiveRecord::RecordInvalid: Validation failed: Default pull rate is not a number
Passing model specs:
it "is invalid without a default_pull_rate" do
expect(FactoryGirl.build(:player, default_pull_rate: nil)).to_not be_valid
end
it "is invalid when default_pull_rate is a string" do
expect(FactoryGirl.build(:player, default_pull_rate: "fast")).to_not be_valid
end
Typo in player_params?
def player_params
params.require(:player).permit(:name, :default_pull_rated)
end
should be _rate not _rated
def player_params
params.require(:player).permit(:name, :default_pull_rate)
end
I have looked at similar errors but not only will my test wont pass, the script will not sign in a user.
Failures:
Finished in 0.41649 seconds 31 examples, 2 failures
Failed examples:
rspec ./spec/controllers/sessions_controller_spec.rb:48 #
SessionsController GET 'new' POST 'create' success should sign the
user in rspec ./spec/controllers/sessions_controller_spec.rb:54 #
SessionsController GET 'new' POST 'create' success should redirect to
the user show page
Done.
Error upon signin: NoMethodError in SessionsController#create
undefined method `authenticate' for #
Rails.root: /Users/lancevelasco/Development/appsample
Application Trace | Framework Trace | Full Trace
app/controllers/sessions_controller.rb:10:in `create'
Code
user.rb
# == Schema Information
#
# Table name: users
#
# id :integer not null, primary key
# name :string(255)
# email :string(255)
# created_at :datetime not null
# updated_at :datetime not null
# encrypted_password :string(255)
# salt :string(255)
#
class User < ActiveRecord::Base
attr_accessor :password
attr_accessible :email, :name, :password, :password_confirmation
email_regex = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :name, :presence => true,
:length => { :maximum => 50 }
validates :email, :presence => true,
:format => { :with => email_regex },
:uniqueness => { :case_sensitive => false }
validates :password, :presence => true,
:confirmation => true,
:length => { :within => 6..40 }
before_save :encrypt_password
def has_password?(submitted_password)
encrypted_password == encrypt(submitted_password)
end
def User.authenticate(email, submitted_password)
user = find_by_email(email)
return nil if user.nil?
return user if user.has_password?(submitted_password)
end
def authenticate_with_salt(id, cookie_salt)
user = find_by_id(id)
(user && user.salt == cookie_salt ) ? user : nil
end
private
def encrypt_password
self.salt = make_salt if new_record?
self.encrypted_password = encrypt(password)
end
def encrypt(string)
secure_hash("#{salt}--#{string}")
end
def make_salt
secure_hash("#{Time.now.utc}--#{password}")
end
def secure_hash(string)
Digest::SHA2.hexdigest(string)
end
end
sessions_controller.rb
class SessionsController < ApplicationController
def new
#title = "Sign in"
end
def create
user = User.authenticate(params[:session][:email],
params[:session][:password])
if user.nil?
flash.now[:error] = "Invalid email/password combination."
render 'new'
else
sign_in user
redirect_back_or user
end
end
def destroy
sign_out
redirect_to root_path
end
end
sessions_helper.rb
module SessionsHelper
def sign_in_(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
current_user = user
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user || user_from_remember_token
end
private
def user_from_remember_token
User.authenticate_with_salt()
end
def remember_token
cookies.signed[:remember_token] || [nil,nil]
end
end
user_controller_spec.rb
require 'spec_helper'
describe SessionsController do
render_views
describe "GET 'new'" do
it "returns http success" do
get 'new'
response.should be_success
end
it "should have the right title" do
get :new
response.should have_selector('title', :content => "Sign in")
end
describe "POST 'create'" do
describe "failure" do
before(:each) do
#attr = { :email => "", :password => ""}
end
it "should re-render the new page" do
post :create, :session => #attr
response.should render_template('new')
end
it "should have the right title" do
post :create, :session => #attr
end
it "should have an error message" do
post :create, :session => #attr
flash.now[:error].should =~ /invalid/i
end
end
describe "success" do
before(:each) do
#user= Factory(:user)
#attr = { :email => #user.email, :password => #user.password }
end
it "should sign the user in" do
post :create, :session => #attr
controller.current_user.should == #user
controller.should be_signed_in
end
it "should redirect to the user show page" do
post :create, :session => #attr
response.should redirect_to(user_path(#user))
end
end
end
end
end
Now you call authenticate on object of User, but if I remember right, you declared authenticate as method of User class (like static method in java, for instance), not an object of User. That is why you get NoMethodError and so you should use something like Kubee method call:
user = User.authenticate(params[:session][:email], params[:session][:password])
From what I understand, Michael Hartl used to use bcrypt to handle his authentication (has_secure_password"). Looks like he opted to drop this and write his own authentication (looks like in order to add a salt...very good indeed).
You have in user.rb:
def User.authenticate(email, submitted_password)
user = find_by_email(email)
return nil if user.nil?
return user if user.has_password?(submitted_password)
end
So as you can see, you need to pass the email to the authenticate method as well, and since it also grabs the user, you can simplify the session#create method. Try this:
def create
user = User.authenticate(params[:session][:email], params[:session][:password])
if user.nil?
flash.now[:error] = 'Invalid email/password combination'
render 'new'
else
sign_in user
redirect_back_or user
end
end
It was a rather simple fix.
In session_helper
def sign_in_(user)
should of read
def sign_in(user)
And in the application_controller
add
include SessionsHelper
yes , i Successfully, thank you.
i was used this
def User.authenticate(email, submitted_password)
user = find_by_email(email)
return nil if user.nil?
return user if user.has_password?(submitted_password)
end
and then
def create
user= User.authenticate(params[:session][:email]
params[:session][:password])
if user.nil?
flash.now[:error] ="Invalid email/password combination."
#title = "sign in"
render 'new'
else
sign_in user
redirect_to user
end
end
I have an rspec/factory girl test where I can't get a test to pass.
I'm using devise where current_user calls the currently logged in User model.
I can load up a test console and type in
u = Factory(:user)
u.company
And this will return a valid company but for some reason in rspec calling current_user.company is returning nil.
Any ideas?
Controller
class CompaniesController < ApplicationController
before_filter :authenticate_user!
def show
#company = current_user.company
end
end
Model
class User < ActiveRecord::Base
validates_uniqueness_of :email, :case_sensitive => false
has_one :company
end
Factory
Factory.define :company do |f|
f.name 'Test Company'
end
Factory.sequence(:email) do |n|
"person#{n}#example.com"
end
Factory.define :user do |f|
f.name 'Test User'
f.email {Factory.next :email}
f.password 'password'
f.company Factory(:company)
end
Test
describe CompaniesController do
before(:each) do
#user = Factory(:user)
sign_in #user
end
describe "GET show" do
before do
get :show
end
it "should find the users company" do
assigns(:company).should be_a(Company)
end
end
end
Spec Helper
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
config.infer_base_class_for_anonymous_controllers = false
end
Test Result
Failures:
1) CompaniesController GET show should find the users company
Failure/Error: assigns(:company).should be_a(Company)
expected nil to be a kind of Company(id: integer, name: string, user_id: integer, created_at: datetime, updated_at: datetime)
# ./spec/controllers/companies_controller_spec.rb:21:in `block (3 levels) in <top (required)>'
EDIT
I have removed the f.company = Factory(:company) in the factories file. And made my controller spec this
require 'spec_helper'
describe CompaniesController do
let(:current_user) { Factory(:user) }
before(:each) do
sign_in current_user
current_user.company = Factory(:company)
current_user.save
end
describe "GET show" do
before do
get :show
end
it "should find the users company" do
current_user.should respond_to(:company)
assigns(:company).should == current_user.company
end
end
end
I'm not sure but I believe assigns(:company) checks for an instance variable #company which obviously doesn't exist. Try putting #company = #user.company in your before(:each) block or test for it in another way, for example;
it "should find the users company" do
#user.should respond_to(:company)
end
I believe that should do it!
Define Let object for company in your controller rspec.
describe CompaniesController do
describe "authorizations" do
before(:each) do
let(:company) { Factory :company }
let(:user_admin) { Factory(:user) }
end
it "should redirect" do
sign_in(user_admin)
get :show
end
it "should find the users company" do
assigns(:company).should be_a(company)
end
end
end
Can you try with above spec ?
I think the main thing you were missing is setting up an association in your factory. Starting from your original example:
Factory.define :user do |f|
f.name 'Test User'
f.email {Factory.next :email}
f.password 'password'
f.association :company, factory => :company
end
Then when you create a user, it will create a company and fill in user.company_id with the proper id.
See "Associations" in the Factory Girl Getting Started doc.
I'm following http://ruby.railstutorial.org/chapters/following-users#code:from_users_followed_by_final and these tests fails:
describe "status feed" do
it "should have a feed" do
#user.should respond_to(:feed)
end
it "should include the user's microposts" do
#user.feed.include?(#mp1).should be_true
#user.feed.include?(#mp2).should be_true
end
it "should not include a different user's microposts" do
mp3 = Factory(:micropost,
:user => Factory(:user, :email => Factory.next(:email)))
#user.feed.include?(mp3).should be_false
end
end
end
Here are the errors:
1) status feed should have a feed
Failure/Error: #user.should respond_to(:feed)
expected nil to respond to :feed
# ./spec/models/user_spec.rb:170
2) status feed should include the user's microposts
Failure/Error: #user.feed.should include(#mp1)
NoMethodError:
undefined method `feed' for nil:NilClass
# ./spec/models/user_spec.rb:174
3) status feed should not include a different user's microposts
Failure/Error: #user.feed.should_not include(mp3)
NoMethodError:
undefined method `feed' for nil:NilClass
# ./spec/models/user_spec.rb:181
4) status feed should include the microposts of followed users
Failure/Error: #user.follow!(followed)
NoMethodError:
undefined method `follow!' for nil:NilClass
# ./spec/models/user_spec.rb:187
Here's my micropost.rb file:
class Micropost < ActiveRecord::Base
attr_accessible :content
belongs_to :user
validates :content, :presence => true, :length => { :maximum => 140 }
validates :user_id, :presence => true
def self.from_users_followed_by(user)
followed_ids = user.following.map(&:id).join(", ")
where("user_id IN (#{followed_ids}) OR user_id = ?", user)
end
default_scope :order => 'microposts.created_at DESC'
# Return microposts from the users being followed by the given user.
scope :from_users_followed_by, lambda { |user| followed_by(user) }
private
# Return an SQL condition for users followed by the given user.
# We include the user's own id as well.
def self.followed_by(user)
followed_ids = %(SELECT followed_id FROM relationships
WHERE follower_id = :user_id)
where("user_id IN (#{followed_ids}) OR user_id = :user_id",
{ :user_id => user })
end
end
Here's my home method in my pages_controller.rb file:
def home
#title = "Home"
if signed_in?
#micropost = Micropost.new
#feed_items = current_user.feed.paginate(:page => params[:page])
end
end
Here's my feed method in user.rb
def feed
Micropost.from_users_followed_by(self)
end
Let me know if I should reproduce more code.
You're not setting #user before referencing it in your tests. This needs to be set to a User object. This is set in the describe "relationsips" block earlier, but doesn't appear to be set there.