require 'spec_helper' describe Granite::Form::Model::Associations::EmbedsMany do before do stub_model(:dummy) do include Granite::Form::Model::Associations end stub_model(:project) do include Granite::Form::Model::Lifecycle attribute :title, String validates :title, presence: true end stub_model(:user) do include Granite::Form::Model::Persistence include Granite::Form::Model::Associations attribute :name, String embeds_many :projects end end let(:user) { User.new(name: 'User') } let(:association) { user.association(:projects) } let(:existing_user) { User.instantiate name: 'Rick', projects: [{title: 'Genesis'}] } let(:existing_association) { existing_user.association(:projects) } context 'performers' do let(:user) { User.new(projects: [Project.new(title: 'Project 1')]) } specify do p2 = user.projects.build(title: 'Project 2') p3 = user.projects.build(title: 'Project 3') p4 = user.projects.create(title: 'Project 4') expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 4'}]) p2.save! expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 2'}, {'title' => 'Project 4'}]) p2.destroy!.destroy! expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 4'}]) user.projects.create(title: 'Project 5') expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 4'}, {'title' => 'Project 5'}]) p3.destroy! user.projects.first.destroy! expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 4'}, {'title' => 'Project 5'}]) p4.destroy!.save! expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 4'}, {'title' => 'Project 5'}]) expect(user.projects.count).to eq(5) user.projects.map(&:save!) expect(user.read_attribute(:projects)).to eq([ {'title' => 'Project 1'}, {'title' => 'Project 2'}, {'title' => 'Project 3'}, {'title' => 'Project 4'}, {'title' => 'Project 5'} ]) user.projects.map(&:destroy!) expect(user.read_attribute(:projects)).to eq([]) user.projects.first(2).map(&:save!) expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 1'}, {'title' => 'Project 2'}]) expect(user.projects.reload.count).to eq(2) p3 = user.projects.create!(title: 'Project 3') expect(user.read_attribute(:projects)).to eq([ {'title' => 'Project 1'}, {'title' => 'Project 2'}, {'title' => 'Project 3'} ]) p3.destroy! expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 1'}, {'title' => 'Project 2'}]) user.projects.create!(title: 'Project 4') expect(user.read_attribute(:projects)).to eq([ {'title' => 'Project 1'}, {'title' => 'Project 2'}, {'title' => 'Project 4'} ]) end end context 'callbacks' do before do User.class_eval do embeds_many :projects, before_add: ->(object) { callbacks.push([:before_add, object]) }, after_add: ->(object) { callbacks.push([:after_add, object]) } collection :callbacks, Array end end let(:project1) { Project.new(title: 'Project1') } let(:project2) { Project.new(title: 'Project2') } specify do expect { association.build(title: 'Project1') } .to change { user.callbacks } .to([[:before_add, project1], [:after_add, project1]]) end specify do expect do association.build(title: 'Project1') association.build(title: 'Project2') end .to change { user.callbacks } .to([ [:before_add, project1], [:after_add, project1], [:before_add, project2], [:after_add, project2] ]) end specify do expect { association.create(title: 'Project1') } .to change { user.callbacks } .to([[:before_add, project1], [:after_add, project1]]) end specify do expect do association.create(title: 'Project1') association.create(title: 'Project2') end .to change { user.callbacks } .to([ [:before_add, project1], [:after_add, project1], [:before_add, project2], [:after_add, project2] ]) end specify do expect { association.concat(project1, project2) } .to change { user.callbacks } .to([ [:before_add, project1], [:after_add, project1], [:before_add, project2], [:after_add, project2] ]) end specify do expect do association.concat(project1) association.concat(project2) end .to change { user.callbacks } .to([ [:before_add, project1], [:after_add, project1], [:before_add, project2], [:after_add, project2] ]) end specify do expect { association.writer([project2, project1]) } .to change { user.callbacks } .to([ [:before_add, project2], [:after_add, project2], [:before_add, project1], [:after_add, project1] ]) end specify do expect do association.writer([project1]) association.writer([]) association.writer([project2]) end .to change { user.callbacks } .to([ [:before_add, project1], [:after_add, project1], [:before_add, project2], [:after_add, project2] ]) end context 'default' do before do User.class_eval do embeds_many :projects, before_add: ->(owner, object) { owner.callbacks.push([:before_add, object]) }, after_add: ->(owner, object) { owner.callbacks.push([:after_add, object]) }, default: -> { {title: 'Project1'} } collection :callbacks, Array end end specify do expect { association.concat(project2) } .to change { user.callbacks } .to([ [:before_add, project2], [:after_add, project2], [:before_add, project1], [:after_add, project1] ]) end end end describe 'user#association' do specify { expect(association).to be_a described_class } specify { expect(association).to eq(user.association(:projects)) } end describe 'project#embedder' do let(:project) { Project.new(title: 'Project') } specify { expect(association.build.embedder).to eq(user) } specify { expect(association.create.embedder).to eq(user) } specify do expect { association.writer([project]) } .to change { project.embedder }.from(nil).to(user) end specify do expect { association.concat(project) } .to change { project.embedder }.from(nil).to(user) end specify do expect { association.target = [project] } .to change { project.embedder }.from(nil).to(user) end context 'default' do before do User.class_eval do embeds_many :projects, default: -> { {title: 'Project1'} } end end specify { expect(association.target.first.embedder).to eq(user) } context do before do User.class_eval do embeds_many :projects, default: -> { Project.new(title: 'Project1') } end end specify { expect(association.target.first.embedder).to eq(user) } end end context 'embedding goes before attributes' do before do Project.class_eval do attribute :title, String, normalize: ->(value) { "#{value}#{embedder.name}" } end end specify { expect(association.build(title: 'Project').title).to eq('ProjectUser') } specify { expect(association.create(title: 'Project').title).to eq('ProjectUser') } end end describe '#build' do specify { expect(association.build).to be_a Project } specify { expect(association.build).not_to be_persisted } specify do expect { association.build(title: 'Swordfish') } .not_to change { user.read_attribute(:projects) } end specify do expect { association.build(title: 'Swordfish') } .to change { association.reader.map(&:attributes) } .from([]).to([{'title' => 'Swordfish'}]) end specify do expect { existing_association.build(title: 'Swordfish') } .not_to change { existing_user.read_attribute(:projects) } end specify do expect { existing_association.build(title: 'Swordfish') } .to change { existing_association.reader.map(&:attributes) } .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Swordfish'}]) end end describe '#create' do specify { expect(association.create).to be_a Project } specify { expect(association.create).not_to be_persisted } specify { expect(association.create(title: 'Swordfish')).to be_a Project } specify { expect(association.create(title: 'Swordfish')).to be_persisted } specify do expect { association.create } .not_to change { user.read_attribute(:projects) } end specify do expect { association.create(title: 'Swordfish') } .to change { user.read_attribute(:projects) } .from(nil).to([{'title' => 'Swordfish'}]) end specify do expect { association.create(title: 'Swordfish') } .to change { association.reader.map(&:attributes) } .from([]).to([{'title' => 'Swordfish'}]) end specify do expect { existing_association.create } .not_to change { existing_user.read_attribute(:projects) } end specify do expect { existing_association.create(title: 'Swordfish') } .to change { existing_user.read_attribute(:projects) } .from([{title: 'Genesis'}]).to([{title: 'Genesis'}, {'title' => 'Swordfish'}]) end specify do expect { existing_association.create(title: 'Swordfish') } .to change { existing_association.reader.map(&:attributes) } .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Swordfish'}]) end end describe '#create!' do specify { expect { association.create! }.to raise_error Granite::Form::ValidationError } specify { expect(association.create!(title: 'Swordfish')).to be_a Project } specify { expect(association.create!(title: 'Swordfish')).to be_persisted } specify do expect { muffle(Granite::Form::ValidationError) { association.create! } } .not_to change { user.read_attribute(:projects) } end specify do expect { muffle(Granite::Form::ValidationError) { association.create! } } .to change { association.reader.map(&:attributes) }.from([]).to([{'title' => nil}]) end specify do expect { association.create!(title: 'Swordfish') } .to change { user.read_attribute(:projects) }.from(nil).to([{'title' => 'Swordfish'}]) end specify do expect { association.create!(title: 'Swordfish') } .to change { association.reader.map(&:attributes) } .from([]).to([{'title' => 'Swordfish'}]) end specify do expect { muffle(Granite::Form::ValidationError) { existing_association.create! } } .not_to change { existing_user.read_attribute(:projects) } end specify do expect { muffle(Granite::Form::ValidationError) { existing_association.create! } } .to change { existing_association.reader.map(&:attributes) } .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => nil}]) end specify do expect { existing_association.create!(title: 'Swordfish') } .to change { existing_user.read_attribute(:projects) } .from([{title: 'Genesis'}]).to([{title: 'Genesis'}, {'title' => 'Swordfish'}]) end specify do expect { existing_association.create!(title: 'Swordfish') } .to change { existing_association.reader.map(&:attributes) } .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Swordfish'}]) end end describe '#apply_changes' do specify do association.build expect { association.apply_changes } .not_to change { association.target.map(&:persisted?) } .from([false]) end specify do association.build(title: 'Genesis') expect { association.apply_changes } .to change { association.target.map(&:persisted?) } .from([false]).to([true]) end specify do existing_association.target.first.mark_for_destruction existing_association.build(title: 'Swordfish') expect { existing_association.apply_changes } .to change { existing_association.target.map(&:title) } .to(['Swordfish']) end specify do existing_association.target.first.mark_for_destruction existing_association.build(title: 'Swordfish') expect { existing_association.apply_changes } .to change { existing_association.target.map(&:persisted?) } .from([true, false]).to([true]) end specify do existing_association.target.first.mark_for_destruction existing_association.build(title: 'Swordfish') expect { existing_association.apply_changes } .to change { existing_association.destroyed.map(&:title) } .from([]).to(['Genesis']) end specify do existing_association.target.first.destroy! existing_association.build(title: 'Swordfish') expect { existing_association.apply_changes } .to change { existing_association.target.map(&:title) } .to(['Swordfish']) end specify do existing_association.target.first.destroy! existing_association.build(title: 'Swordfish') expect { existing_association.apply_changes } .to change { existing_association.destroyed.map(&:title) } .from([]).to(['Genesis']) end end describe '#apply_changes!' do specify do association.build expect { association.apply_changes! } .to raise_error Granite::Form::AssociationChangesNotApplied end specify do association.build(title: 'Genesis') expect { association.apply_changes! } .to change { association.target.map(&:persisted?) } .to([true]) end specify do existing_association.target.first.mark_for_destruction existing_association.build(title: 'Swordfish') expect { existing_association.apply_changes! } .to change { existing_association.target.map(&:title) } .to(['Swordfish']) end specify do existing_association.target.first.mark_for_destruction existing_association.build(title: 'Swordfish') expect { existing_association.apply_changes! } .to change { existing_association.target.map(&:persisted?) } .from([true, false]).to([true]) end specify do existing_association.target.first.destroy! existing_association.build(title: 'Swordfish') expect { existing_association.apply_changes! } .to change { existing_association.target.map(&:title) } .to(['Swordfish']) end end describe '#target' do specify { expect(association.target).to eq([]) } specify { expect(existing_association.target).to eq(existing_user.projects) } specify { expect { association.build }.to change { association.target.count }.to(1) } end describe '#default' do before { User.embeds_many :projects, default: -> { {title: 'Default'} } } before do Project.class_eval do include Granite::Form::Model::Primary primary :title end end let(:new_project) { Project.new.tap { |p| p.title = 'Project' } } let(:existing_user) { User.instantiate name: 'Rick' } specify { expect(association.target.map(&:title)).to eq(['Default']) } specify { expect(association.target.map(&:new_record?)).to eq([true]) } specify { expect { association.replace([new_project]) }.to change { association.target.map(&:title) }.to eq(['Project']) } specify { expect { association.replace([]) }.to change { association.target }.to([]) } specify { expect(existing_association.target).to eq([]) } specify { expect { existing_association.replace([new_project]) }.to change { existing_association.target.map(&:title) }.to(['Project']) } specify { expect { existing_association.replace([]) }.not_to change { existing_association.target } } context do before { Project.send(:include, Granite::Form::Model::Dirty) } specify { expect(association.target.any?(&:changed?)).to eq(false) } end end describe '#loaded?' do specify { expect(association.loaded?).to eq(false) } specify { expect { association.target }.to change { association.loaded? }.to(true) } specify { expect { association.build }.to change { association.loaded? }.to(true) } specify { expect { association.replace([]) }.to change { association.loaded? }.to(true) } specify { expect { existing_association.replace([]) }.to change { existing_association.loaded? }.to(true) } end describe '#reload' do specify { expect(association.reload).to eq([]) } specify { expect(existing_association.reload).to eq(existing_user.projects) } context do before { association.build(title: 'Swordfish') } specify do expect { association.reload } .to change { association.reader.map(&:attributes) }.from([{'title' => 'Swordfish'}]).to([]) end end context do before { existing_association.build(title: 'Swordfish') } specify do expect { existing_association.reload } .to change { existing_association.reader.map(&:attributes) } .from([{'title' => 'Genesis'}, {'title' => 'Swordfish'}]).to([{'title' => 'Genesis'}]) end end end describe '#clear' do specify { expect(association.clear).to eq(true) } specify { expect { association.clear }.not_to change { association.reader } } specify { expect(existing_association.clear).to eq(true) } specify do expect { existing_association.clear } .to change { existing_association.reader.map(&:attributes) }.from([{'title' => 'Genesis'}]).to([]) end specify do expect { existing_association.clear } .to change { existing_user.read_attribute(:projects) }.from([{title: 'Genesis'}]).to([]) end context do let(:existing_user) { User.instantiate name: 'Rick', projects: [{title: 'Genesis'}, {title: 'Swordfish'}] } before { Project.send(:include, Granite::Form::Model::Callbacks) } if ActiveModel.version >= Gem::Version.new('5.0.0') before { Project.before_destroy { throw :abort } } else before { Project.before_destroy { false } } end specify { expect(existing_association.clear).to eq(false) } specify do expect { existing_association.clear } .not_to change { existing_association.reader } end specify do expect { existing_association.clear } .not_to change { existing_user.read_attribute(:projects) } end end end describe '#reader' do specify { expect(association.reader).to eq([]) } specify { expect(existing_association.reader.first).to be_a Project } specify { expect(existing_association.reader.first).to be_persisted } context do before { association.build } specify { expect(association.reader.last).to be_a Project } specify { expect(association.reader.last).not_to be_persisted } specify { expect(association.reader.size).to eq(1) } specify { expect(association.reader(true)).to eq([]) } end context do before { existing_association.build(title: 'Swordfish') } specify { expect(existing_association.reader.size).to eq(2) } specify { expect(existing_association.reader.last.title).to eq('Swordfish') } specify { expect(existing_association.reader(true).size).to eq(1) } specify { expect(existing_association.reader(true).last.title).to eq('Genesis') } end end describe '#writer' do let(:new_project1) { Project.new(title: 'Project 1') } let(:new_project2) { Project.new(title: 'Project 2') } let(:invalid_project) { Project.new } specify do expect { association.writer([Dummy.new]) } .to raise_error Granite::Form::AssociationTypeMismatch end specify { expect { association.writer(nil) }.to raise_error NoMethodError } specify { expect { association.writer(new_project1) }.to raise_error NoMethodError } specify { expect(association.writer([])).to eq([]) } specify { expect(association.writer([new_project1])).to eq([new_project1]) } specify do expect { association.writer([new_project1]) } .to change { association.reader.map(&:attributes) }.from([]).to([{'title' => 'Project 1'}]) end specify do expect { association.writer([new_project1]) } .not_to change { user.read_attribute(:projects) } end specify do expect { existing_association.writer([new_project1, invalid_project]) } .to raise_error Granite::Form::AssociationChangesNotApplied end specify do expect { muffle(Granite::Form::AssociationChangesNotApplied) { existing_association.writer([new_project1, invalid_project]) } } .not_to change { existing_user.read_attribute(:projects) } end specify do expect { muffle(Granite::Form::AssociationChangesNotApplied) { existing_association.writer([new_project1, invalid_project]) } } .not_to change { existing_association.reader } end specify do expect { existing_association.writer([new_project1, Dummy.new, new_project2]) } .to raise_error Granite::Form::AssociationTypeMismatch end specify do expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.writer([new_project1, Dummy.new, new_project2]) } } .not_to change { existing_user.read_attribute(:projects) } end specify do expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.writer([new_project1, Dummy.new, new_project2]) } } .not_to change { existing_association.reader } end specify { expect { existing_association.writer(nil) }.to raise_error NoMethodError } specify do expect { muffle(NoMethodError) { existing_association.writer(nil) } } .not_to change { existing_user.read_attribute(:projects) } end specify do expect { muffle(NoMethodError) { existing_association.writer(nil) } } .not_to change { existing_association.reader } end specify { expect(existing_association.writer([])).to eq([]) } specify do expect { existing_association.writer([]) } .to change { existing_user.read_attribute(:projects) }.to([]) end specify do expect { existing_association.writer([]) } .to change { existing_association.reader }.to([]) end specify { expect(existing_association.writer([new_project1, new_project2])).to eq([new_project1, new_project2]) } specify do expect { existing_association.writer([new_project1, new_project2]) } .to change { existing_association.reader.map(&:attributes) } .from([{'title' => 'Genesis'}]).to([{'title' => 'Project 1'}, {'title' => 'Project 2'}]) end specify do expect { existing_association.writer([new_project1, new_project2]) } .to change { existing_user.read_attribute(:projects) } .from([{title: 'Genesis'}]).to([{'title' => 'Project 1'}, {'title' => 'Project 2'}]) end end describe '#concat' do let(:new_project1) { Project.new(title: 'Project 1') } let(:new_project2) { Project.new(title: 'Project 2') } let(:invalid_project) { Project.new } specify do expect { association.concat(Dummy.new) } .to raise_error Granite::Form::AssociationTypeMismatch end specify { expect { association.concat(nil) }.to raise_error Granite::Form::AssociationTypeMismatch } specify { expect(association.concat([])).to eq([]) } specify { expect(existing_association.concat([])).to eq(existing_user.projects) } specify { expect(existing_association.concat).to eq(existing_user.projects) } specify { expect(association.concat(new_project1)).to eq([new_project1]) } specify do expect { association.concat(new_project1) } .to change { association.reader.map(&:attributes) }.from([]).to([{'title' => 'Project 1'}]) end specify do expect { association.concat(new_project1) } .not_to change { user.read_attribute(:projects) } end specify { expect(existing_association.concat(new_project1, invalid_project)).to eq(false) } specify do expect { existing_association.concat(new_project1, invalid_project) } .to change { existing_user.read_attribute(:projects) } .from([{title: 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Project 1'}]) end specify do expect { existing_association.concat(new_project1, invalid_project) } .to change { existing_association.reader.map(&:attributes) } .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Project 1'}, {'title' => nil}]) end specify do expect { existing_association.concat(new_project1, Dummy.new, new_project2) } .to raise_error Granite::Form::AssociationTypeMismatch end specify do expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.concat(new_project1, Dummy.new, new_project2) } } .not_to change { existing_user.read_attribute(:projects) } end specify do expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.concat(new_project1, Dummy.new, new_project2) } } .to change { existing_association.reader.map(&:attributes) } .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Project 1'}]) end specify do expect(existing_association.concat(new_project1, new_project2)) .to eq([existing_user.projects.first, new_project1, new_project2]) end specify do expect { existing_association.concat([new_project1, new_project2]) } .to change { existing_association.reader.map(&:attributes) } .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Project 1'}, {'title' => 'Project 2'}]) end specify do expect { existing_association.concat([new_project1, new_project2]) } .to change { existing_user.read_attribute(:projects) } .from([{title: 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Project 1'}, {'title' => 'Project 2'}]) end end end