Produce a NestedHstore hash with Rails fixtures - ruby-on-rails

I have a field called custom that is defined as a NestedHstore:
# project.rb
serialize :custom, ActiveRecord::Coders::NestedHstore
I create fixtures for this model for testing:
one:
company: one
user: one
custom:
:name:
:prompt: 'What is your name?'
When this is run, the nested hashes are saved as strings, i.e. #project.custom = {"name"=>"{:prompt=>\"What is your name?\"}"}
I've gotten around this in unit and functional tests by adding a prepare_custom method that manually reconstructs the nested hash from the string BEFORE anything runs.
def prepare_custom(p)
if p.custom.present?
if p.custom.is_a? Hash
p.custom.each do |k,v|
if (v.class == String) && (v[0] == "{") && (v[-1] == "}")
p.custom[k] = eval(v)
end
end
end
p.save
p.reload
puts "*** project: #{p.inspect}"
end
end
When I run integration tests (with Capybara/Poltergeist/Puma), this happens:
prepare_custom(#project) seems to work. p.inspect at the end shows that custom is a properly nested hash.
The integration test code that runs immediately after prepare_custom seems to completely ignore that the project has been changed. It thinks p.custom[:name] is a string again.
How can I either get fixtures to save the nested hash properly, or get my integration tests to reflect the work done in prepare_custom?
Follow-up
After trying: custom: <%= {name: { prompt: 'What is your name?'}}.to_yaml.inspect %> in the fixture, we get:
ActiveRecord::StatementInvalid: ActiveRecord::StatementInvalid: PG::InternalError: ERROR: Syntax error near ':' at position 4
LINE 1: ...company_id") VALUES ('Simple prompt', 0, '---
^
: INSERT INTO "projects" ("name", "custom", "created_at", "updated_at", "id", "company_id") VALUES ('Simple prompt', '---
:name:
:prompt: What is your name?
', '2017-05-17 21:15:25', '2017-05-17 21:15:25', 159160234, 980190962, 159160234)

Related

Rails fixture associations

Struggling to get fixtures to associate. We are (finally!) writing tests for an existing app. We are using Minitest as the framework.
mail_queue_item.rb:
class MailQueueItem < ApplicationRecord
belongs_to :order
...
end
mail_queue_items.yml:
one:
run_at: 2015-01-04 10:22:19
mail_to: test#test.com
order: seven_days_ago
email_template: with_content
customer: basic_customer
status: waiting
orders.yml:
seven_days_ago:
tenant: basic_tenant
ecom_store: basic_store
ecom_order_id: 123-123456-123456
purchase_date: <%= 7.days.ago %>
set_to_shipped_at: <%= 6.days.ago %>
ecom_order_status: shipped
fulfillment_channel: XYZ
customer: basic_customer
In a test:
require 'test_helper'
class MailQueueItemDenormalizerTest < ActiveSupport::TestCase
fixtures :mail_queue_items, :customers, :email_templates, :orders
test 'should make hash' do
#mqi = mail_queue_items(:one)
puts #mqi.order_id.inspect
puts #mqi.order.inspect
order = orders(:seven_days_ago)
puts order.inspect
assert #mqi.order.ecom_order_status == 'shipped'
end
end
The output looks like this:
MailQueueItemDenormalizerTest
447558226
nil
#<Order id: 447558226, tenant_id: 926560165, customer_id: 604023446, ecom_order_id: "123-123456-123456", purchase_date: "2022-08-13 19:18:02.000000000 -0700", last_update_date: nil, ecom_order_status: "shipped", fulfillment_channel: "XYZ", ....>
test_should_make_hash ERROR (5.96s)
Minitest::UnexpectedError: NoMethodError: undefined method `ecom_order_status' for nil:NilClass
test/denormalizers/mail_queue_item_denormalizer_test.rb:26:in `block in <class:MailQueueItemDenormalizerTest>'
So even though the order_id on the mail_queue_item is correct (it matches the id from the object loaded from the fixture) the association does not work.
I have tried the suggestions in Nil Associations with Rails Fixtures... how to fix? of putting ids in everything and the result is the same.
Project is in Rails 6 (long project that started life in Rails 3.1).
The issue turned out to be that the fixtures were creating invalid objects. The objects were valid enough to get written to the database, but were not passing the Rails validations.
The resulting behavior is quite odd I think, but I don't know of a better way to do it.
I discovered this by adding:
puts "#mqi.order.valid? = #{#mqi.order.valid?}"
puts "#mqi.customer.valid? = #{#mqi.customer.valid?}"
puts "#mqi.email_template.valid? = #{#mqi.email_template.valid?}"
puts #mqi.email_template.errors.full_messages
code in there. Yes, it's disgusting.

How to setup fixtures when using Mobility with a Key Value backend?

Got a question on how to setup fixtures for Mobility. Would be very grateful for any tips on how to get this going and would be a valuable lesson for me as well on how to tackle setting up fixtures in general.
Not using any gems to setup fixtures, just the default Rails approach for this case. I have a Song model which has multiple translatable attributes, title uses Mobility, description and content use Mobility Action Text.
It works really well but when setting up fixtures I'm finding it difficult to relate the records. There's three tables at play here songs where the only field used is status. mobility_string_translations stores translations for title and action_text_rich_texts stores translated descriptions and content.
This is how my translation setup looks like in Song:
class Song < ApplicationRecord
extend Mobility
validates :title_pt, presence: true
validates :status, inclusion: { in: %w(draft published private) },
presence: true
translates :title, type: :string, locale_accessors: I18n.available_locales
translates :description, backend: :action_text, locale_accessors: I18n.available_locales
translates :content, backend: :action_text, locale_accessors: I18n.available_locales
# file continuation...
As for fixtures songs.yml looks like this:
one:
status: "published"
Then based on what I've found online I've created mobility/string_translations.yml with the following content:
one:
translatable_id: one (Song)
translatable_type: "Song"
key: "title"
value: "Title in English"
locale: "en"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
two:
translatable_id: one (Song)
translatable_type: "Song"
key: "title"
value: "Titulo em Português"
locale: "pt"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
This seems to work but I know it isn't because when I inspect #song = songs(:one) looking for translated values (#song.title_pt and #song.title_en) they're both nil.
Any idea on what to do here? 🙏
I think the issue is with how you declared the translatable relationship in the mobility_string_translation table.
It should be either the fully explicit form.
translatable_id: 1
translatable_type: Song
Or the shorthand provided by fixtures.
translatable: one (Song)
Here you are kind of mixing both.
It's documented here in the Polymorphic belongs_to https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html.
I've managed to make the explicit form work but not the shorthand for some reason.
I've been looking for instructions on how to make fixtures work with mobility myself and you provided the heavy lifting with your example so thanks a lot for that.
The issue in my case is that translatable_type was Song instead of "Song" and it couldn't map the records in mobility_string_translations to the correct Song record. Here's a bit more detail on the setup that I have that does work to write tests:
By work I mean, the Mobility translation records defined in fixture files are detected and can be used to compose tests. Running #song.title_en should output a value instead of nil.
Let's consider the following Song model, it has a title that can be translated and a status which is only used to affect the visibility of the song in the front end. Fixtures for a couple of Songs would look like this:
# test/fixtures/songs.yml
one:
id: 1
status: "published"
two:
id: 2
status: "draft"
The id is usually not specified in fixtures but here it becomes necessary so that we're sure which identifier to use when pointing translated records.
The Mobility implementation will store any translated titles at mobility_string_translations the following can be added to test/fixtures/mobility/string_translations.yml:
# test/fixtures/mobility/string_translations.yml
song_one_en:
translatable_id: 1
translatable_type: "Song"
key: "title"
value: "Maçaranduba Wood"
locale: "en"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
song_one_pt:
translatable_id: 1
translatable_type: "Song"
key: "title"
value: "Madeira de Maçaranduba"
locale: "pt"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
song_two_en:
translatable_id: 2
translatable_type: "Song"
key: "title"
value: "Dona Maria from Camboatá"
locale: "en"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
song_two_pt:
translatable_id: 2
translatable_type: "Song"
key: "title"
value: "Dona Maria do Camboatá"
locale: "pt"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
Each song includes a title for English and Portuguese in this case but any locales the record is going to make use of or need to be tested can be included here, in an individual record.
The important aspect here is that all translatable_type columns are explicit string types.
For example, do "Song", instead of Song when adding a value to the property.
Setting up fixtures with this method associates translated properties to a record and enables them to be accessed in a test.
For example, to change the title of a song, the record can be brought into the test in a setup block and the title translations will be available and can be modified:
# test/controllers/song_controller_test.rb
require "test_helper"
class SongControllerTest < ActionDispatch::IntegrationTest
setup do
#song = songs(:one)
end
test "admin can edit a song" do
# Keeps a copy of the original record for comparison.
current_record = #song
# Passes the locale to the request helper to keep it from getting confused with the record id.
# Changes the title of the record.
patch song_url(I18n.locale, #song, { song: { title_en: 'Updated Song Title' } })
# Retrieves the same record to be used for comparison.
updated_record = Song.find(#song.id)
# Checks that a change actually occurred.
assert current_record.updated_at != updated_record.updated_at
# Checks that the list of songs is being displayed to the user.
assert_redirected_to songs_path
end
end
To make sure that the fixture has setup the association bettween the model and the translated records, the debugger method can be used. Start by adding it as a breakpoint to your test logic, in this case I'm going to use the example above:
# test/controllers/song_controller_test.rb
test "admin can edit a song" do
current_record = #song
patch song_url(I18n.locale, #song, { song: { title_en: 'Updated Song Title' } })
updated_record = Song.find(#song.id)
debugger # <-- The script will pause here.
assert current_record.updated_at != updated_record.updated_at
assert_redirected_to songs_path
end
Then the test can be run, bin/rails test would work but in this example the command to run just the tests for this file would be:
bin/rails test test/controllers/role_controller_test.rb
The output in the terminal will look similar to this, the program will be paused at this point and it is interactive:
bin/rails test test/controllers/song_controller_test.rb
Running 26 tests in a single process (parallelization threshold is 50)
Run options: --seed 56548
# Running:
.............[64, 73] in ~/Projects/rails_app/test/controllers/song_controller_test.rb
64|
65| current_record = #song
66| patch song_url(I18n.locale, #song, { song: { title_en: 'Updated Song Title' } })
67| updated_record = Song.find(#song.id)
68|
=> 69| debugger # <-- The script will pause here.
70|
71| # Checks that a change actually occurred.
72| assert current_record.updated_at != updated_record.updated_at
73|
=>
#0 block in <class:SongControllerTest> at ~/Projects/rails_app/test/controllers/song_controller_test.rb:69
#1 block in run (3 levels) at ~/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.15.0/lib/minitest/test.rb:98
# and 24 frames (use `bt' command for all frames)
(rdbg)
Any variables defined before debugger can be accessed, this can be used to inspect if #song was changed:
(rdbg) #song.title_en # ruby
"Updated Song Title"
(rdbg) #song.title_pt # ruby
"Madeira de Maçaranduba"
The title was updated using the patch request defined in the test case. Typing continue will move on from the breakpoint and continue running the code in the test file.
That should be it!

Why does Rails pass "0" from a select statement to ActiveRecord?

I have been working on this for 2 hours and still have found no solution or any reasoning to why this is happening.
in my model I have
models/course.rb
class Course < ActiveRecord::Base
validates! :province, inclusion: { :in => ['Alberta','British Columbia','Manitoba','New Brunswick','Newfoundland and Labrador','Nova Scotia','Ontario','Prince Edward Island','Quebec','Saskatchewan', 'Province'], message: "%{value} is not included in the list" }
end
in my view I have this select statement
= form_for #course, url: teach_path do |f|
= f.select :province, options_for_select(['Alberta','British Columbia','Manitoba','New Brunswick','Newfoundland and Labrador','Nova Scotia','Ontario','Prince Edward Island','Quebec','Saskatchewan'], "#{#course.province.to_s}"), {include_blank: "Choose your Province"}, {required: :required}
= f.submit
However when I submit the form I get this error:
ActiveModel::StrictValidationFailed in CoursesController#create
Province 0 is not included in the list
I have tried even defining my own method in the active record model
def includes_province
unless ['Alberta','British Columbia','Manitoba','New Brunswick','Newfoundland and Labrador','Nova Scotia','Ontario','Prince Edward Island','Quebec','Saskatchewan', 'Province'].include?(province.to_s)
errors.add(:base, "There is no #{province} in the list")
end
end
It just returns
There is no 0 in the list
Why is province always 0 no matter what? I have checked the logs and it shows:
Processing by CoursesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"***", "course"=>{"province"=>"Manitoba"}, "commit"=>"submit"}
Indeed as #spickermann says ensure your province field is not Integer in DB. Check migration that create Course table (something like CreateCourses) in rails_app/db/migrate folder, or simply check schema.rb file under rails_app/db/ folder. Check whether it says
t.integer :province
If so, your column is interger and by assigning it a string (any string) it would actually set 0 to integer value.
To replicate same thing, try calling "rails console" (or "rails c") in your OS command line and then typing
c = Course.take # takes random course from db and loads it to c var
c.province = "Manitoba" # assign string value to the field - like it happens in form
c.province # will actually print 0 !
Once you change column type to t.string instead of i.integer it should work.

Rails: how can I assert that MySQL throws a duplicate entry error

I've got a uniqueness constraint on my FooBar table (at the database level).
# I have this in my migration
add_index :foo_bars, [:foo_id, :bar_id], :unique => true
I'm pleased to say that MySQL does it's job, and reliably prevents duplicate entries in this table. But how can I test that? Here's what I've tried:
test 'can not be a dup' do
assert_no_difference('FooBar.count') do
FooBar.create do |sc|
sc.foo = foos(:one)
sc.bar = bars(:one)
end
end
end
This runs with the following output:
FooBar#test_can_not_be_a_dup:
ActiveRecord::RecordNotUnique: Mysql2::Error: Duplicate entry '206867376-519457691' for key 'index_foo_bars_on_foo_id_and_bar_id'
So the test is not completing.
Can I run my test inside a transaction or something, to a) ensure that it's rolled back, and b) ensure that the reason for the rollback is an ActiveRecord::RecordNotUnique: Mysql2::Error?
Or should I just trust MySQL/ActiveRecord to have my back on this?
Fixed:
assert_raises ActiveRecord::RecordNotUnique do
FooBar.create do |sc|
sc.foo = foos(:one)
sc.bar = bars(:one)
end
end

Rails Seed-Fu Writer why seed got commented out?

So, here's my custom rake task:
task :backup => :environment do |t|
SeedFu::Writer.write('/path/to/file.rb', class_name: 'Category', constraints: [:id] do |w|
Category.all.each do |x|
w << x
end
end
end
And the following result file contains:
# DO NOT MODIFY THIS FILE, it was auto-generated.
#
# Date: 2014-06-15 21:08:13 +0700
# Seeding Category
# Written with the command:
#
# /home/dave/.rvm/gems/ruby-2.1.2/bin/rake backup
#
Category.seed(:id,
#<Category id: 1, name: "foo">,
#<Category id: 2, name: "bar">,
#<Category id: 3, name: "baz">
)
# End auto-generated file.
Question: Why did the seedfile got commented out?
Thanks!
So, this is a basic string manipulation.
When I read closely on their source code, seed method accepts Hash, not object.
So, I simply translated the object to its Hash equivalent:
task :backup => :environment do |t|
SeedFu::Writer.write('/path/to/file.rb', class_name: 'Category', constraints: [:id] do |w|
Category.all.each do |x|
w << x.as_json
end
end
end
Note that you can use .attributes or .as_json, but I read somewhere that .attributes actually takes a lot more time than .as_json.
After that, I encountered yet another problem: Datetime columns. When converted to JSON, Datetime columns are not quoted. So what I did was:
Store the column names (of type Datetime) to an array.
Store current object's Hash to a local variable.
Convert the Datetime values to String, using .to_s (in the local variable)
Output the modified local variable to writer object.
Hope this helps.
Experiencing exact the same problems, commented output and datetime columns are not quoted. It seems that ActiveSupport::JSON could kill two birds with one stone.
require 'seed-fu'
j = ActiveSupport::JSON
SeedFu::Writer.write("#{Rails.root}/db/dump_seeds/lectures.rb",{ class_name: 'Lecture', constraints: [:id, :name]}) do |writer|
Lecture.all.order('id').each do |e|
writer << j.decode(j.encode(e))
end
end

Resources