I'm trying to create a mailer and allow a status to be changed to pending upon creation. So I have the following in my controller:
class Api::UserTrainingResourcesController < Api::BaseController
respond_to :html, :json
def create
#user_training_resource = UserTrainingResource::Create.call(user_training_resource_params)
respond_with(#user_training_resource)
end
def destroy
#user_training_resource = UserTrainingResource.find_by(params[:id])
#user_training_resource.destroy
end
private
def user_training_resource_params
params.require(:user_training_resources).permit(:training_resources_id, :status).merge(spud_user_id: current_user_id)
end
end
Then in my Operations I have the following create:
class UserTrainingResource
class Create < Operation
def call(params)
params[:status] = :pending
training_resource = UserTrainingResource.new(params)
UserTrainingResourceMailer.requested(training_resource).deliver_later if training_resource.save
training_resource
end
end
end
My model has:
class UserTrainingResource < ApplicationRecord
belongs_to :user
belongs_to :training_resource
enum status: [:pending, :confirmed, :rejected], _prefix: :status
scope :employee, -> { where.not(status: ['rejected']) }
scope :hr, -> { where(status: SameScope::HR) }
module SameScope
HR = ['pending', 'confirmed', 'rejected'].freeze
end
def view_user_training_resource_request?(user)
return true if user.human_resources && SameScope::HR.include?(status)
false
end
def change_status?(user, status)
return true if user.human_resources && SameScope::HR.include?(status)
false
end
end
Then in my test I have:
require 'rails_helper'
RSpec.describe UserTrainingResource::Create do
let(:params) { attributes_for(:user_training_resource).merge(user_id: create(:user).id) }
describe '#call' do
it 'queues a mailer' do
ut = UserTrainingResource::Create.call(params)
expect(UserTrainingResourceMailer).to send_mail(:requested)
expect(ut.status).to eq('pending')
end
end
end
So this ends up with a Failure/Error: expect(ut.status).to eq('pending') Expect: "pending", got" nil. (compared using ==)
I thought the issue was that pending was not saving to the db but that's because I had status present as a string not an integer when using enum in the model. But it's still getting nil.
Edit:
After noticing that I had status using string I went ahead and added in a migration file to change it to integer. Test failed with the same issue. Tried to rollback my test environment, failed. Did a rake db:reset. Re-added in data in both development and test. Still getting the issue of essentially:
Failure/Error: expect(ut.status).to eq('pending')
expected: "pending"
got: nil
(compared using ==)
Related
In a controller I have an update method which creates a record (call it book), associates it to an existing record (call it author) and saves it.
Book belongs to one Author
add_author_to_book_controller.rb
def update
#author = App::Models::Author.new(params)
#book = App::Models::Book.where(id: params[:book_id]).first
#book.author = #author
#book.save!
# this works fine...
# puts #book.author.inspect
render json: { status: :ok }
end
add_author_to_book_controller_spec.rb
describe App::AddAuthorToBookController do
describe '#update' do
# this is a contrived example, there is more setup regarding creating the "book" properly...
let(:name) { 'foobar' }
let(:action) { xhr :put, :update, params }
let(:params) { { first_name: name } }
subject { book }
before { action }
it { expect(response.status).to eq 200 }
it 'should save the author to the book' do
# why is author nil here?
# puts book.author.inspect
expect(book.author.first_name).to eq name
end
end
end
I tried book.reload in the test but that didn't work. I'm new to rails, is there some conventional way of testing an associated record in a controller test?
Wasn't saving author before associating it to book...
def update
#author = App::Models::Author.new(params)
# was simply missing this
#author.save!
#book = App::Models::Book.where(id: params[:book_id]).first
#book.author = #author
#book.save!
# this works fine...
# puts #book.author.inspect
render json: { status: :ok }
end
First of all I recommend you to make your controllers more general because that is the correct architecture you need to follow, so your controller can be called authors_controller.rb and manage all authors stuff or books_controller.rb and manage all books stuff. And following this approach you can have a method associate_book which receives an author and a book and creates the right association. Let me explain it with code:
class Author < ApplicationRecord
has_many :books
# Fields :name
validates :name, presence: true
end
class Book < AoplicationRecord
# Optional because I think you want to add the author after create it
belongs_to :author, optional: true
# Fields :title, :publish_year, :author_id
validates :title, :publish_year, :author_id, presence: true
end
class AuthorsController < ApplicationController
def associate_book
# params here will contain the following [:author_id, book_id]
author = Author.find(params[:author_id])
book = Book.find(params[:book_id])
book.author = author
book.save!
rescue ActiveRecord::RecordInvalid => error
# This will allow you to catch exceptions related to the update
end
end
Then you can test this method by doing the following, supposing that this method will be called from a route
# Testing with RSpec
# spec/controllers/authors_controller.rb
RSpec.describe AuthorsController do
let(:author) { Author.first }
let(:book) { Book.first }
it 'Should associate an author with a provided book' do
expect do
post :associate_book, params: { author_id: author.id, book_id: book.id }
end.to change { author.books.count }.by(1)
end
end
This will check the total count of books associated to the author.
I am struggling with understanding how to save a model to MongoDB using mongoid and rails_admin.
I've got my model:
class PictureAsset < ActiveRecord::Base
include Mongoid::Document
field :data_file_name, type: String
field :data_content_type, type: String
field :data_file_size, type: Integer
end
This was made through a generation:
bundle exec rails g model PictureAsset data_file_name:string data_content_type:string data_file_size:integer
Rails admin side loads up just fine, and I can navigate to the index page for PictureAsset.
When I try to access my custom action, asset_action, I get the following error:
undefined method `belongs_to' for #<RailsAdmin::AbstractModel:0xbf2e791>
My action looks like this:
module RailsAdmin
module Config
module Actions
class AssetAction < RailsAdmin::Config::Actions::Base
RailsAdmin::Config::Actions.register(self)
register_instance_option :collection do
true
end
register_instance_option :http_methods do
[:get, :put]
end
register_instance_option :controller do
proc do
if request.get? # EDIT
binding.pry
respond_to do |format|
format.html { render #action.template_name }
format.js { render #action.template_name, layout: false }
end
elsif request.put? # UPDATE
binding.pry
#newUserPropVal.save
end
end
end
register_instance_option :link_icon do
'icon-list-alt'
end
end
end
end
end
Can someone please explain what I'm doing wrong here?
Your model includes code for both ActiveRecord and Mongoid. Pick one and remove code for the other.
ActiveRecord will look like this:
class PictureAsset < ActiveRecord::Base
# No field definitions needed
end
Mongoid will look like this:
# Does not derive from anything
class PictureAsset
include Mongoid::Document
field :data_file_name, type: String
field :data_content_type, type: String
field :data_file_size, type: Integer
end
This may not solve the error you are getting but is a good start.
Anyone know why an after_create callback is triggered only on production environment ?
class Schedule < ActiveRecord::Base
belongs_to :account
validates :schedule_type, presence: true # default = 'appointment'
after_create :send_notification, if: :devices_and_appointment?
def devices_and_appointment?
account_has_devices? && schedule_type.appointment?
end
def account_has_devices?
!account.devices.blank?
end
def appointment?
schedule_type.eql?('appointment')
end
end
On production, the callback is triggered, even if schedule_type is not appointment. I check the logs and the controller receive the params as expected (schedule_type != appointment)
For a strange reason, when after_create is invoked, the schedule_type is the default.
We check this on development and even in staging env and we cannot reproduce the bug.
The differences between staging and production is the DB sharding (Octopus) but we only have this activated for reads, in some other controllers, not even in the ScheduleController
We end up moving the callback method to a service, calling the function after the .save and that fix the problem, but I still want to know why this happen.
Controller:
def create
#schedule = Schedule.new(schedule_params)
if #schedule.save
render json: #schedule, status: 201
else
render json: { errors: #schedule.errors }, status: 422
end
end
def schedule_params
params
.require(schedule)
.permit(:account_id, :cause, :ends_at, :schedule_type, :starts_at)
end
Request:
Parameters: {"schedule"=>{"cause"=>"cause random", "starts_at"=>"2018-09-17T21:00:00.000-05:00", "ends_at"=>"2018-09-17T21:30:00.000-05:00", "schedule_type"=>"blocked", "account_id"=>"123"}}
I just built a Rails 5 new app --api. I scaffolded a model and added an enum.
class Track < ApplicationRecord
enum surface_type: [:asphalt, :gravel, :snow], _prefix: true
end
One of the scaffolded controller test looks like this:
context "with invalid params" do
it "assigns a newly created but unsaved track as #track" do
post :create, params: {track: invalid_attributes}, session: valid_session
expect(assigns(:track)).to be_a_new(Track)
end
end
I added invalid attributes at the top:
let(:invalid_attributes) {{
"name": "Brands Hatch",
"surface_type": "wood"
}}
and changed the expect line to this
expect(assigns(:track)).not_to be_valid
But the test does not work, because its not possible to create a Track object if you pass an invalid enum.
Controller action:
def create
#track = Track.new(track_params)
if #track.save
render json: #track, status: :created
else
render json: #track.errors, status: :unprocessable_entity
end
end
So how do I test this scenario?
One way you could trap an invalid :surface_type through normal validation is by intercepting the assignment.
class Track < ApplicationRecord
enum surface_type: [:asphalt, :gravel, :snow], _prefix: true
attr_accessor :bad_surface_type
validate :check_surface_type
def surface_type=(surface)
super surface
rescue
self.bad_surface_type = surface
super nil
end
private
def check_surface_type
errors.add(:surface_type, "the value #{bad_surface_type} is not valid") if bad_surface_type
end
end
I have:
class Competitor < ActiveRecord::Base
after_commit :delayed_create_matching_surveys
end
class Survey < ActiveRecord::Base
after_create :create_matching_results, :touch_review
end
class Result < ActiveRecord::Base
after_update :update_inputs
def update_inputs
if input_id.present?
#input = Input.find(input_id)
if survey.reload.results.sort_by{|r| r.points}.last.selected
competitor.inputs << #input unless competitor.inputs.include?(#input)
else
competitor.inputs.delete(#input) if competitor.inputs.include?(#input)
end
end
binding.pry_remote ## << which exists purely to slow down the app
end
end
I have configured Sidekiq to execute all delayed methods immediately in the test environment. I have also tried removing the delay_ and have confirmed this does not solve the problem.
With this code, the following spec passes:
describe "update_inputs" do
before :each do
#product = create(:product)
#question = create(:question, :product => #product)
#review = create(:review, :product => #product)
#client = #review.competitors.first
#competitor = create(:competitor, :review => #review)
#category = create(:category)
#input1 = create(:input)
#input2 = create(:input)
#competitor.categories << #category
#input1.categories << #category
#input2.categories << #category
end
it "assigns one input to a competitor when one is selected" do
#survey = #competitor.reload.surveys.select{|s| s.input_id == #input1.id}.first
#survey.results.select{|r| r.name.include?("sells this product")}.first.update_attributes :selected => true
#competitor.inputs.should == [#input1]
end
end
All I have to do is type pry-remote and then exit when Rspec gets to binding.pry_remote.
It seems all it's doing is slowing my app down long enough for Rspec to catch up.
But without binding.pry_remote, the spec fails. Without it, #competitor.inputs is [], as is #competitor.reload.inputs and #competitor.inputs(true) and #competitor.reload.inputs(true).
How can I fix this?