Rspec controller test failing post #create with association - ruby-on-rails

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.

Related

NoMethodError: undefined method `access_token' for nil:NilClass in RSpec

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

Rails RSpec test - prevent delete comment for user who is not the author of it

I'm trying to test the 'destroy' action for my nested comments controller.
In my filmweb app I have scope and validations which prevents users from deleting a comment which is not the author. In web version everything works well but I don't know how to test this case.
Here is my comments_controller
def destroy
#comment = #movie.comments.find(params[:id])
if #comment.destroy
flash[:notice] = 'Comment successfully deleted'
else
flash[:alert] = 'You are not the author of this comment'
end
redirect_to #movie
end
Comment model
class Comment < ApplicationRecord
belongs_to :user
belongs_to :movie
validates :body, presence: true
validates :user, :movie, presence: true
validates :user, uniqueness: { scope: :movie }
scope :persisted, -> { where.not(id: nil) }
end
User model has_many :comments, dependent: :destroy Movie model has_many :comments, dependent: :destroy .
I'm using devise and FactoryBot, specs are here:
describe "DELETE #destroy" do
let(:user) { FactoryBot.create(:user) }
let(:movie) { FactoryBot.create(:movie) }
let(:other_user) { FactoryBot.create(:user, user_id: 100)}
it "doesn't delete comment" do
sign_in(other_user)
comment = FactoryBot.create(:comment, movie: movie, user: user)
expect do
delete :destroy, params: { id: comment.id, movie_id: movie.id }
end.to_not change(Comment, :count)
expect(flash[:alert]).to eq "You are not the author of this comment"
end
end
I've got an error undefined method `user_id=' for #<User:0x00007fb049644d20> and no idea what is the good way to do so.
===EDIT===
Here is my FactoryBot
FactoryBot.define do
factory :user do
email { Faker::Internet.email }
password "password"
confirmed_at 1.day.ago
end
factory :unconfirmed_user do
email { Faker::Internet.email }
password "password"
end
end
The problem is that the the users table does not have a user_id column which you are trying to use in the other_user instance, the column name is simply id:
let(:other_user) { FactoryBot.create :user, id: 100 }
You can leave out the id entirely, it will get a different id automatically:
let(:other_user) { FactoryBot.create :user }

Rspec POST Test not passing. Expected response to be a <3XX: redirect>, but was a 200

This POST create test is not working. It should redirect to reports_path, but it does not work.
describe "POST #create" do
let(:report) { assigns(:report) }
let(:test_option) { create(:option) }
let(:test_student) { create(:student) }
context "when valid" do
before(:each) do
post :create, params: {
report: attributes_for(:report, student: test_student,
report_options_attributes: [build(:report_option).attributes]
)
}
end
it "should redirect to reports_path" do
expect(response).to redirect_to reports_path
end
My params are set like this:
def report_params
params.require(:report).permit(:student,
report_options_attributes: [:id, :option, :note, :_destroy]
)
end
Controller:
def create
#report = Report.new(report_params)
if #report.save
redirect_to reports_path
else
render :new
end
end
Report model:
class Report < ApplicationRecord
belongs_to :student, dependent: :delete
has_many :report_options, dependent: :destroy
accepts_nested_attributes_for :report_options, allow_destroy: true
end
ReportOption model:
class ReportOption < ApplicationRecord
belongs_to :option
belongs_to :report, optional: true
end
I am passing the correct params in the test, but I really do not know what is going wrong..
I've had a similar problem. I fixed it passing just the ids in the params like that:
def report_params
params.require(:report).permit(:student_id,
report_options_attributes: [:id, :option_id, :note, :_destroy]
)
end
Don't know if it will work for you. You also need to change the student: test_student to student_id: test_student.id inside your before(:each).

ActionDispatch::Http::UploadedFile being passed as a string during integration test

I have an Album model and a child Picture model, whereby a file upload is handled by carrierwave.
The app runs as intended, however fails when I try to write an integration test for the upload.
From the development log, working parameters for this appear as follows:
Parameters: ..., "images"=>[#<ActionDispatch::Http::UploadedFile:0x007f8a42be2c78>...
During the integration test, im getting parameters as follows:
Parameters: ..., "images"=>["#<ActionDispatch::Http::UploadedFile:0x00000004706fd0>"...
As you can see, in the test scenario, #<ActionDispatch::Http::UploadedFile is being passed through as a string (confirmed with byebug), which is causing the test to fail.
How do I get the test to pass this as the object?
Integration Test
require 'test_helper'
class AlbumsCreationTest < ActionDispatch::IntegrationTest
def setup
#user = users(:archer)
#file ||= File.open(File.expand_path(Rails.root + 'test/fixtures/cat1.jpg', __FILE__))
#testfile ||= uploaded_file_object(PicturesUploader, :image, #file)
end
test "should create new album with valid info" do
log_in_as(#user)
assert_difference 'Album.count', 1 do #This assertion fails
post user_albums_path(#user), album: { title: 'test',
description: 'test',
}, images: [#testfile]
end
assert_redirected_to #user
end
end
from test_helper.rb
# Upload File (Carrierwave) Ref: http://nicholshayes.co.uk/blog/?p=405
def uploaded_file_object(klass, attribute, file, content_type = 'image/jpeg')
filename = File.basename(file.path)
klass_label = klass.to_s.underscore
ActionDispatch::Http::UploadedFile.new(
tempfile: file,
filename: filename,
head: %Q{Content-Disposition: form-data; name="#{klass_label}[#{attribute}]"; filename="#{filename}"},
type: content_type
)
end
Models
class Album < ActiveRecord::Base
belongs_to :user
has_many :pictures, dependent: :destroy
accepts_nested_attributes_for :pictures, allow_destroy: true
validates_presence_of :pictures
end
class Picture < ActiveRecord::Base
belongs_to :album
mount_uploader :image, PicturesUploader
validates_integrity_of :image
validates_processing_of :image
validates :image, presence: true,
file_size: { less_than: 10.megabytes }
end
Controller
class AlbumsController < ApplicationController
before_action :valid_user, only: [:new, :create, :edit, :update, :destroy]
def create
#album = current_user.albums.build(album_params)
if params[:images]
params[:images].each { |file|
debugger #file.class is String in integration test
#album.pictures.build(image: file)
}
end
if #album.save
# end
flash[:success] = "Album Created!"
redirect_to current_user
else
flash[:alert] = "Something went wrong."
render :new
end
end
private
def album_params
params.require(:album).permit(:user_id, :title, :description, :price,
pictures_attributes: [:id, :image, :image_cache, :_destroy])
end
def valid_user
#user = User.friendly.find(params[:user_id])
redirect_to(root_url) unless #user == current_user
end
end
Solved:
It appears that the uploaded_file_object method doesn't work at the integration test level.
I went back to using fixture_file_upload and everything works as intended.

Passing a valid attribute to controller but failing validation

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

Resources