require 'minitest/autorun' require 'prism' require 'tmpdir' class OrmNestedAttributesTest < Minitest::Test def setup @dir = Dir.mktmpdir('prism-nested-attrs-store') @store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore')) @store.create_table(:posts, %w[id title]) @store.create_table(:comments, %w[id body post_id]) @store.create_table(:accounts, %w[id owner_id plan]) @store.create_table(:users, %w[id name]) post_class = Class.new(Prism::Orm::Model) do table_name :posts has_many :comments, class_name: 'Comment', foreign_key: 'post_id', dependent: :destroy accepts_nested_attributes_for :comments, allow_destroy: true, reject_if: :all_blank, limit: 3 end comment_class = Class.new(Prism::Orm::Model) do table_name :comments belongs_to :post, class_name: 'Post', foreign_key: 'post_id' validates :body, presence: true, length: { minimum: 2 } end user_class = Class.new(Prism::Orm::Model) do table_name :users has_one :account, class_name: 'Account', foreign_key: 'owner_id' accepts_nested_attributes_for :account end account_class = Class.new(Prism::Orm::Model) do table_name :accounts belongs_to :owner, class_name: 'User', foreign_key: 'owner_id' validates :plan, presence: true end [post_class, comment_class, user_class, account_class].each { |klass| klass.store = @store } Object.const_set(:Post, post_class) Object.const_set(:Comment, comment_class) Object.const_set(:User, user_class) Object.const_set(:Account, account_class) end def teardown FileUtils.remove_entry(@dir) %i[Post Comment User Account].each { |name| Object.send(:remove_const, name) if Object.const_defined?(name) } end def test_builds_and_saves_new_children_of_a_new_parent post = Post.create!( title: 'hello', comments_attributes: [{ body: 'first' }, { body: 'second' }] ) assert_equal 2, Comment.where(post_id: post.id).count bodies = Comment.where(post_id: post.id).map(&:body).sort assert_equal %w[first second], bodies end def test_accepts_indexed_hash_form_like_a_rails_form_builder post = Post.create!( title: 'hello', comments_attributes: { '0' => { body: 'first' }, '1' => { body: 'second' } } ) assert_equal 2, Comment.where(post_id: post.id).count end def test_updates_an_existing_child_by_id post = Post.create!(title: 'hello') comment = Comment.create!(body: 'original', post_id: post.id) post.update!(comments_attributes: [{ id: comment.id, body: 'edited' }]) assert_equal 'edited', comment.reload.body end def test_allow_destroy_removes_the_child post = Post.create!(title: 'hello') comment = Comment.create!(body: 'doomed', post_id: post.id) post.update!(comments_attributes: [{ id: comment.id, _destroy: '1' }]) assert_nil Comment.store.find(Comment.table_name, comment.id) end def test_destroy_flag_is_ignored_without_allow_destroy no_destroy_post_class = Class.new(Prism::Orm::Model) do table_name :posts has_many :comments, class_name: 'Comment', foreign_key: 'post_id' accepts_nested_attributes_for :comments end no_destroy_post_class.store = @store post = no_destroy_post_class.create!(title: 'hello') comment = Comment.create!(body: 'kept', post_id: post.id) post.update!(comments_attributes: [{ id: comment.id, _destroy: '1', body: 'still here' }]) refute_nil Comment.store.find(Comment.table_name, comment.id) assert_equal 'still here', comment.reload.body end def test_reject_if_all_blank_skips_empty_new_records post = Post.create!(title: 'hello', comments_attributes: [{ body: '' }, { body: 'real' }]) assert_equal 1, Comment.where(post_id: post.id).count assert_equal 'real', Comment.where(post_id: post.id).first.body end def test_limit_raises_when_exceeded assert_raises(Prism::Orm::NestedAttributes::TooManyRecords) do Post.create!(title: 'hello', comments_attributes: [{ body: 'a' }, { body: 'b' }, { body: 'c' }, { body: 'd' }]) end end def test_invalid_nested_record_blocks_the_whole_save_and_reports_errors post = Post.new(title: 'hello', comments_attributes: [{ body: 'good enough' }, { body: 'x' }]) refute post.save assert post.new_record? assert post.errors.key?(:comments) assert_equal 0, Comment.count end def test_id_pointing_at_another_parents_child_is_rejected_not_hijacked post_a = Post.create!(title: 'a') post_b = Post.create!(title: 'b') foreign_comment = Comment.create!(body: 'belongs to a', post_id: post_a.id) refute post_b.update(comments_attributes: [{ id: foreign_comment.id, body: 'hijacked' }]) assert_equal 'belongs to a', foreign_comment.reload.body assert_equal post_a.id, foreign_comment.reload.post_id end def test_id_on_a_brand_new_parent_is_rejected post = Post.new(title: 'hello', comments_attributes: [{ id: 999, body: 'ghost' }]) refute post.save assert post.errors.key?(:comments) end def test_has_one_nested_attributes_builds_the_single_record user = User.create!(name: 'ada', account_attributes: { plan: 'pro' }) account = Account.find_by(owner_id: user.id) refute_nil account assert_equal 'pro', account.plan end def test_has_one_nested_attributes_updates_the_existing_record user = User.create!(name: 'ada') account = Account.create!(plan: 'free', owner_id: user.id) user.update!(account_attributes: { id: account.id, plan: 'pro' }) assert_equal 'pro', account.reload.plan end def test_does_not_support_associations_other_than_has_many_or_has_one assert_raises(ArgumentError) do Class.new(Prism::Orm::Model) do table_name :comments belongs_to :post, class_name: 'Post', foreign_key: 'post_id' accepts_nested_attributes_for :post end end end end