Problems with validates_inclusion_of, acts_as_tree and rspec - ruby-on-rails

I have problems to get rspec running properly to test validates_inclusion_of my migration looks like this:
class CreateCategories < ActiveRecord::Migration
def self.up
create_table :categories do |t|
t.string :name
t.integer :parent_id
t.timestamps
end
end
def self.down
drop_table :categories
end
end
my model looks like this:
class Category < ActiveRecord::Base
acts_as_tree
validates_presence_of :name
validates_uniqueness_of :name
validates_inclusion_of :parent_id, :in => Category.all.map(&:id), :unless => Proc.new { |c| c.parent_id.blank? }
end
my factories:
Factory.define :category do |c|
c.name "Category One"
end
Factory.define :category_2, :class => Category do |c|
c.name "Category Two"
end
my model spec looks like this:
require 'spec_helper'
describe Category do
before(:each) do
#valid_attributes = {
:name => "Category"
}
end
it "should create a new instance given valid attributes" do
Category.create!(#valid_attributes)
end
it "should have a name and it shouldn't be empty" do
c = Category.new :name => nil
c.should be_invalid
c.name = ""
c.should be_invalid
end
it "should not create a duplicate names" do
Category.create!(#valid_attributes)
Category.new(#valid_attributes).should be_invalid
end
it "should not save with invalid parent" do
parent = Factory(:category)
child = Category.new #valid_attributes
child.parent_id = parent.id + 100
child.should be_invalid
end
it "should save with valid parent" do
child = Factory.build(:category_2)
child.parent = Factory(:category)
# FIXME: make it pass, it works on cosole, but I don't know why the test is failing
child.should be_valid
end
end
I get the following error:
'Category should save with valid
parent' FAILED Expected #<Category id:
nil, name: "Category Two", parent_id:
5, created_at: nil, updated_at: nil>
to be valid, but it was not Errors:
Parent is missing
On console everything seems to be fine and work as expected:
c1 = Category.new :name => "Parent Category"
c1.valid? #=> true
c1.save #=> true
c1.id #=> 1
c2 = Category.new :name => "Child Category"
c2.valid? #=> true
c2.parent_id = 100
c2.valid? #=> false
c2.parent_id = 1
c2.valid? #=> true
I'm running rails 2.3.5, rspec 1.3.0 and rspec-rails 1.3.2
Anybody, any idea?

The problem is that you can't put a call to Category.all.map(&:id) inside the called to validates_inclusion_of.
The first indication that this is the case will be apparent when you try to run
rake db:migrate:down VERSION=<n>
rake db:migrate:up VERSOIN=<n>
where <n> is the version number of the migration that creates the Category model.
You will get something like:
in /Users/sseefried/tmp/so)
== CreateCategories: reverting ===============================================
-- drop_table(:categories)
-> 0.0032s
== CreateCategories: reverted (0.0034s) ======================================
(in /Users/sseefried/tmp/so)
rake aborted!
SQLite3::SQLException: no such table: categories: SELECT * FROM "categories"
(See full trace by running task with --trace)
This is because rake tries to load app/models/category.rb before running the migration. Because the Category model does not exist it fails.
Another way to see the problem is to do tail -f log/development.log and then try to open a console with script/console. You will see an SQL query of the form:
SELECT * FROM "categories"
in the output. This corresponds to the call to Category.all.map(:&id). However, once you start typing commands like:
c1 = Category.new, :name => "Category 1"
you will see that the query SELECT * from "categories" does not reappear in the log. The moral of the story is only constants can appear in calls to validations_inclusion_of because the code in there will only be evaluated once..
The only reason your console code worked was because, in a previous console session, you had created a Category object with id=1
You can write a custom validation that does what you want with:
validate :parent_exists
protected
def parent_exists
ids = Category.all.map(&:id)
if !parent_id.blank? && !ids.member?(parent_id)
errors.add(:parent_id, "does not point to a valid parent record")
end
end
Your rspec tests will pass once you have added this code.

Actually, you can defer enumerable calculation by simply putting Category.all.map(&:id) into a proc/lambda.
Writing
validates_inclusion_of :parent_id,
in: proc{ Category.all.map(&:id) },
unless: proc{ |c| c.parent_id.blank? }
will fetch categories' ids at the time of validation, not at the time of class declaration.

Related

How do I verify PostgreSQL enum constraints from Rails model test?

I am using an enum in Rails and PostgreSQL. In my model tests I usually verify that my Rails validations are backed by database constraints where appropriate (e.g. presence: true in Model and null: false in DB). I do this by deliberately making the model invalid, attempting to save it without validations and making sure it raises a ActiveRecord::StatementInvalid error.
How do I test a PostgreSQL enum from MiniTest? My usual approach isn't working as everything I try to do to set my ActiveRecord model to an invalid enum value raises an ArgumentError, even using write_attribute() directly.
Is there a way to deliberately bypass the enum restrictions in Rails? Do I need to drop down out of ActiveRecord and send an AREL or SQL query direct to the database? Is there some other approach?
# Model
class SisRecord < ApplicationRecord
enum record_type: {
student: "student",
staff: "staff",
contact: "contact"
}
validates :record_type, presence: true
end
# Migration
class CreateSisRecords < ActiveRecord::Migration[7.0]
def change
create_enum :sis_record_type, %w(student staff contact)
create_table :sis_records do |t|
t.enum :record_type, enum_type: :sis_record_type, null: false
t.timestamps
end
end
end
# Test
require "test_helper"
class SisRecordTest < ActiveSupport::TestCase
test "a record_type is required" do
record = sis_records(:valid_sis_record)
record.record_type = nil
assert_not record.save, "Saved the SIS Record without a record type"
end
test "a record_type is required by the database too" do
record = sis_records(:valid_sis_record)
record.record_type = nil
assert_raises(ActiveRecord::StatementInvalid) {
record.save(validate: false)
}
end
test "record_type is restricted to accepted values" do
accepted_values = %w(student staff contact)
record = sis_records(:valid_sis_record)
assert_nothing_raised {
record.record_type = accepted_values.sample
}
assert_raises(ArgumentError) {
record.record_type = "something else"
}
end
test "record_type is restricted to accepted values by the database too" do
accepted_values = %w(student staff contact)
record = sis_records(:valid_sis_record)
record.record_type = accepted_values.sample
assert record.save, "Record didn't save despite accepted type value '#{record.record_type}'"
record.write_attribute(:record_type, "nonsense") ### <-- ArgumentError
assert_raises(ActiveRecord::StatementInvalid) {
record.save(validate: false)
}
end
end
I have an answer to my own question, but I'm still open to better answers.
I found a comment on a gist that showed how to fairly simply insert a record with Arel so for now I am using this approach:
# Just the test in question
test "record_type is restricted to accepted values by the database too" do
accepted_values = %w(student staff contact)
table = Arel::Table.new(:sis_records)
manager = Arel::InsertManager.new
manager.insert [
[table[:record_type], accepted_values.sample],
[table[:created_at], Time.now],
[table[:updated_at], Time.now],
]
assert_nothing_raised {
SisRecord.connection.insert(manager.to_sql)
}
manager.insert [
[table[:record_type], "other type"],
[table[:created_at], Time.now],
[table[:updated_at], Time.now],
]
assert_raises(ActiveRecord::StatementInvalid) {
SisRecord.connection.insert(manager.to_sql)
}
end
created_at and updated_at are required fields so we have to add a value for those.
In my real case (not the simplified version I posted above), SisRecord belongs to Person so I had to provide a valid person ID (UUID) too. I did this by grabbing an ID from my people fixtures:
manager.insert [
[table[:record_type], "other type"],
[table[:person_id], people(:valid_person).id], # <--------
[table[:created_at], Time.now],
[table[:updated_at], Time.now],
]

Rails JSON_API create Book with list of Genres

I'm trying to write my test to ensure creating a new book with genres assigned to it works.
I am using Active Model Serializer with the JSON_API structure (http://jsonapi.org/)
Book Model File
class Book < ApplicationRecord
belongs_to :author, class_name: "User"
has_and_belongs_to_many :genres
end
Genre Model File
class Genre < ApplicationRecord
has_and_belongs_to_many :books
end
Book Serializer file
class BookSerializer < ActiveModel::Serializer
attributes :id, :title, :adult_content, :published
belongs_to :author
has_many :genres
end
Test Sample Data
def setup
...
#fantasy = genres(:fantasy)
#newbook = {
title: "Three Little Pigs",
adult_content: false,
author_id: #jim.id,
published: false,
genres: [{title: 'Fantasy'}]
}
end
Test Method
test "book create - should create a new book" do
post books_path, params: #newbook, headers: user_authenticated_header(#jim)
assert_response :created
json = JSON.parse(response.body)
puts "json = #{json}"
assert_equal "Three Little Pigs", json['data']['attributes']['title']
genre_data = json['data']['relationships']['genres']['data']
puts "genre_data = #{genre_data.count}"
assert_equal "Fantasy", genre_data
end
Book Strong Params
def book_params
params.permit(:title, :adult_content, :published, :author_id, :genres)
end
Test Result (console response)
# Running:
......................................................json = {"data"=>{"id"=>"1018350796", "type"=>"books", "attributes"=>{"title"=>"Three Little Pigs", "adult-content"=>false, "published"=>false}, "relationships"=>{"author"=>{"data"=>{"id"=>"1027431151", "type"=>"users"}}, "genres"=>{"data"=>[]}}}}
genre_data = 0
F
Failure:
BooksControllerTest#test_book_create_-_should_create_a_new_book [/Users/warlock/App_Projects/Raven Quill/Source Code/Rails/raven-quill-api/test/controllers/books_controller_test.rb:60]:
Expected: "Fantasy"
Actual: []
bin/rails test test/controllers/books_controller_test.rb:51
Finished in 1.071044s, 51.3518 runs/s, 65.3568 assertions/s.
55 runs, 70 assertions, 1 failures, 0 errors, 0 skips
As you can see from my JSON console log, it appears my genres are not being set(need to scroll to the right in the test output above).
Please ignore this line:
assert_equal "Fantasy", genre_data
I know that's wrong. At the moment, the json is showing genre => {data: []} (empty array), that's the thing I'm trying to solve at the moment.
How do I go about creating a book with genres in this case, any ideas? :D
This is just sad...third time this week, I am answering my own question.
I finally found the answer from this Stackoverflow question:
HABTM association with Strong Parameters is not saving user in Rails 4
Turns out my strong parameters need to be:
def book_params
params.permit(:title, :adult_content, :published, :author_id, {:genre_ids => []})
end
Then my test data can be:
#fantasy = genres(:fantasy)
#newbook = {
title: "Three Little Pigs",
adult_content: false,
author_id: #jim.id,
published: false,
genre_ids: [#fantasy.id]
}
Update my test method to:
test "book create - should create a new book" do
post books_path, params: #newbook, headers: user_authenticated_header(#jim)
assert_response :created
json = JSON.parse(response.body)
assert_equal "Three Little Pigs", json['data']['attributes']['title']
genre = json['data']['relationships']['genres']['data'].first['title']
assert_equal "Fantasy", genre
end
Now my test passes.

Rails migration delete row

I'm trying to run a migration to delete two rows in the database. Here's the migration file ...
class RemoveMenuItem < ActiveRecord::Migration
def up
MenuItem.delete(name: "Validation Settings")
MenuItem.delete(name: "Identifier Lookup")
end
def down
MenuItem.create(name: "Validation Settings", type: "MenuItem", actionable_item_id: 89, actionable_items_count: 0, sequence: 20)
MenuItem.create(name: "Identifier Lookup", type: "MenuItem", actionable_item_id: 89, actionable_items_count: 0, sequence: 30)
end
end
... but I'm getting this error ...
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "id"
LINE 1: ...ERE "actionable_items"."type" IN ('MenuItem') AND "id"."name...
^
: DELETE FROM "actionable_items" WHERE "actionable_items"."type" IN ('MenuItem') AND "id"."name" = 'Validation Settings'/Users/kweihe/.rvm/gems/ruby-2.1.6/gems/activerecord-3.2.22/lib/active_record/connection_adapters/postgresql_adapter.rb:1163:in `async_exec'
Deletes require an identified record to work. Try this:
def up
MenuItem.find_by(name: "Validation Settings").delete
....
I agree with the commenter though - this doesn't seem like something you necessarily want in your migrations. Probably better in a seed file.
SOLUTION
class RemoveActionableItems < ActiveRecord::Migration
class ActionableItem < ActiveRecord::Base
attr_accessible :actionable_item, :name, :sequence, :type
end
def up
validation_settings = MenuItem.find_by_name("Validation Settings")
identifier_lookup = MenuItem.find_by_name("Identifier Lookup")
compliance = InsightReportMenuItem.find_by_name("Compliance")
validation_settings.delete unless validation_settings.nil?
identifier_lookup.delete unless identifier_lookup.nil?
compliance.delete unless compliance.nil?
end
def down
MenuItem.create :name => "Validation Settings", :type => "MenuItem"
MenuItem.create :name => "Identifier Lookup", :type => "MenuItem"
InsightReportMenuItem.create :name => "Compliance", :type => "InsightReportMenuItem"
end
end

Unit Testing validates presence of odd behaviour

I'm trying out the whole TDD and I'm running into a problems with validate presence. I have a model called Event and I want to ensure that when an Event is created that a title a price and a summary exists.
Unit Test Code
class EventTest < ActiveSupport::TestCase
test "should not save without a Title" do
event = Event.new
event.title = nil
assert !event.save, "Save the Event without title"
end
test "should not save without a Price" do
event = Event.new
event.price = nil
assert !event.save, "Saved the Event without a Price"
end
test "should not save without a Summary" do
event = Event.new
event.summary = nil
assert !event.save, "Saved the Event without a Summary"
end
end
I run the test I get 3 FAILS. Which is Good.
Now I want to to just get the title test to pass first with the following code in the Event model.
class Event < ActiveRecord::Base
validates :title, :presence => true
end
When I re-run the test I get 3 PASSES where I would think I should have gotten 1 PASS and 2 FAILS. Why am I getting 3 PASSES?
I have two test helper methods that can make this sort of thing easier to diagnose:
def assert_created(model)
assert model, "Model was not defined"
assert_equal [ ], model.errors.full_messages
assert model.valid?, "Model failed to validate"
assert !model.new_record?, "Model is still a new record"
end
def assert_errors_on(model, *attrs)
found_attrs = [ ]
model.errors.each do |attr, error|
found_attrs << attr
end
assert_equal attrs.flatten.collect(&:to_s).sort, found_attrs.uniq.collect(&:to_s).sort
end
You'd use them in cases like this:
test "should save with a Title, Price or Summary" do
event = Event.create(
:title => 'Sample Title',
:price => 100,
:summary => 'Sample summary...'
)
assert_created event
end
test "should not save without a Title, Price or Summary" do
event = Event.create
assert_errors_on event, :title, :price, :summary
end
This should show if you're missing a validation that you expected and will also give you feedback on specific validations that have failed when not expected.
When you created the model with Event.new, all attributes initially have a value of nil. This means that all 3 attributes you are checking are already nil (so event.title = nil and event.price = nil don't actually do anything). Since title has been marked for validation to ensure its presence, unless you set title to something other than nil, you will not be able to save the model.
Perhaps try adding this to your test class:
setup do
#event_attributes = {:title => "A title", :price => 3.99, :summary => "A summary"}
end
Then instead of:
event = Event.new
event.title = nil
Use:
event = Event.new(#event_attributes.merge(:title => nil))
Do the same for all your tests (substituting :title with whatever attribute you are validating presence for)
Also, there's no reason to call save to test for a valid state. You can just call event.valid? to avoid trips to the database where not needed.

Globalize2 and migrations

I have used globalize2 to add i18n to an old site. There is already a lot of content in spanish, however it isn't stored in globalize2 tables. Is there a way to convert this content to globalize2 with a migration in rails?
The problem is I can't access the stored content:
>> Panel.first
=> #<Panel id: 1, name: "RT", description: "asd", proje....
>> Panel.first.name
=> nil
>> I18n.locale = nil
=> nil
>> Panel.first.name
=> nil
Any ideas?
I'm sure you solved this one way or another but here goes. You should be able to use the read_attribute method to dig out what you're looking for.
I just used the following to migrate content from the main table into a globalize2 translations table.
Add the appropriate translates line to your model.
Place the following in config/initializers/globalize2_data_migration.rb:
require 'globalize'
module Globalize
module ActiveRecord
module Migration
def move_data_to_translation_table
klass = self.class_name.constantize
return unless klass.count > 0
translated_attribute_columns = klass.first.translated_attributes.keys
klass.all.each do |p|
attribs = {}
translated_attribute_columns.each { |c| attribs[c] = p.read_attribute(c) }
p.update_attributes(attribs)
end
end
def move_data_to_model_table
# Find all of the translated attributes for all records in the model.
klass = self.class_name.constantize
return unless klass.count > 0
all_translated_attributes = klass.all.collect{|m| m.attributes}
all_translated_attributes.each do |translated_record|
# Create a hash containing the translated column names and their values.
translated_attribute_names.inject(fields_to_update={}) do |f, name|
f.update({name.to_sym => translated_record[name.to_s]})
end
# Now, update the actual model's record with the hash.
klass.update_all(fields_to_update, {:id => translated_record['id']})
end
end
end
end
end
Created a migration with the following:
class TranslateAndMigratePages < ActiveRecord::Migration
def self.up
Page.create_translation_table!({
:title => :string,
:custom_title => :string,
:meta_keywords => :string,
:meta_description => :text,
:browser_title => :string
})
say_with_time('Migrating Page data to translation tables') do
Page.move_data_to_translation_table
end
end
def self.down
say_with_time('Moving Page translated values into main table') do
Page.move_data_to_model_table
end
Page.drop_translation_table!
end
end
Borrows heavily from Globalize 3 and refinerycms.

Resources