Fixed bug: Dry run imports for resources with has_many associations store the new association value.

- Reworked action and change detection since association changes are not possible to track via ActiveRecord API.

Tests:
- Added import base resource test to ensure associations won't get updated in dry runs.
- Added dry run tests to statistical import factory.
- Fixed broken tests due to switch from .save to .save!
This commit is contained in:
Thorsten Eckel 2017-05-05 10:52:40 +02:00
parent 97114142ed
commit 9c07cecff5
5 changed files with 275 additions and 60 deletions

View file

@ -5,11 +5,10 @@ module Import
attr_reader :resource, :remote_id, :errors
def initialize(resource, *args)
@action = :unknown
handle_args(resource, *args)
initialize_associations_states
import(resource, *args)
return if @resource.blank?
store_associations(:after, @resource)
end
def import_class
@ -26,10 +25,9 @@ module Import
def action
return :failed if errors.present?
return :skipped if !@resource
return :skipped if @resource.blank?
return :unchanged if !attributes_changed?
return :created if created?
:updated
@action
end
def attributes_changed?
@ -48,19 +46,12 @@ module Import
changes = {}
tracked_associations.each do |association|
next if @associations[:before][association] == @associations[:after][association]
next if @associations[:before][association].blank? && @associations[:after][association].blank?
changes[association] = [@associations[:before][association], @associations[:after][association]]
end
changes
end
def created?
return false if @resource.blank?
# dry run
return @resource.created_at.nil? if @resource.changed?
# live run
@resource.created_at == @resource.updated_at
end
private
def initialize_associations_states
@ -91,7 +82,14 @@ module Import
# the record is already created
resource.delete(:created_by_id)
@resource.assign_attributes(resource)
# store the current state of the associations
# from the resource hash because if we assign
# them to the instance some (e.g. has_many)
# will get stored even in the dry run :/
store_associations(:after, resource)
associations = tracked_associations
@resource.assign_attributes(resource.except(*associations))
# the return value here is kind of misleading
# and should not be trusted to indicate if a
@ -99,7 +97,10 @@ module Import
# Use .action instead
return true if !attributes_changed?
@action = :updated
return true if @dry_run
@resource.assign_attributes(resource.slice(*associations))
@resource.save!
true
end
@ -119,18 +120,26 @@ module Import
instance
end
def store_associations(state, instance)
@associations[state] = associations_state(instance)
def store_associations(state, source)
@associations[state] = associations_state(source)
end
def associations_state(instance)
def associations_state(source)
state = {}
tracked_associations.each do |association|
state[association] = instance.send(association)
state[association] = association_value(source, association)
end
state
end
# we have to support instances and (resource) hashes
# here since in case of an update we only have the
# hash as a source but on create we have an instance
def association_value(source, association)
return source[association] if source.is_a?(Hash)
source.send(association)
end
def tracked_associations
# loop over all reflections
import_class.reflect_on_all_associations.collect do |reflection|
@ -150,6 +159,8 @@ module Import
def create(resource, *_args)
@resource = import_class.new(resource)
store_associations(:after, @resource)
@action = :created
return if @dry_run
@resource.save!
external_sync_create(

View file

@ -0,0 +1,14 @@
FactoryGirl.define do
sequence :test_signature_name do |n|
"Test signature #{n}"
end
end
FactoryGirl.define do
factory :signature do
name { generate(:test_signature_name) }
body '#{user.firstname} #{user.lastname}'.text2html
created_by_id 1
updated_by_id 1
end
end

View file

@ -113,6 +113,38 @@ RSpec.describe Import::BaseResource do
ExternalSync.count
}
end
it "doesn't update associations of existing resources" do
# initial import
Import::Test::Group.new(attributes)
group = Group.last
old_signature = create(:signature)
old_users = create_list(:user, 2)
group.update_attribute(:signature_id, old_signature.id)
group.update_attribute(:user_ids, old_users.collect(&:id))
# simulate next import run
travel 20.minutes
new_signature = create(:signature)
new_users = create_list(:user, 2)
attributes[:signature_id] = new_signature.id
attributes[:user_ids] = new_users.collect(&:id)
expect do
Import::Test::Group.new(attributes, dry_run: true)
group.reload
end
.to not_change {
group.signature_id
}
.and not_change {
group.user_ids
}
end
end
end

View file

@ -96,7 +96,7 @@ RSpec.describe Import::Ldap::User do
end
it 'logs failures to HTTP Log' do
expect_any_instance_of(User).to receive(:save).and_raise('SOME ERROR')
expect_any_instance_of(User).to receive(:save!).and_raise('SOME ERROR')
described_class.new(user_entry, ldap_config, user_roles, signup_role_ids)
expect(HttpLog.last.status).to eq('failed')
@ -173,7 +173,7 @@ RSpec.describe Import::Ldap::User do
end
it 'logs failures to HTTP Log' do
expect_any_instance_of(User).to receive(:save).and_raise('SOME ERROR')
expect_any_instance_of(User).to receive(:save!).and_raise('SOME ERROR')
described_class.new(user_entry, ldap_config, user_roles, signup_role_ids)
expect(HttpLog.last.status).to eq('failed')

View file

@ -38,6 +38,8 @@ RSpec.describe Import::StatisticalFactory do
context 'statistics' do
context 'live run' do
it 'tracks created instances' do
Import::Test::GroupFactory.import([attributes])
@ -52,7 +54,8 @@ RSpec.describe Import::StatisticalFactory do
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
it 'tracks updated instances' do
context 'updated instances' do
it 'tracks by regular attributes' do
Import::Test::GroupFactory.import([attributes])
@ -73,6 +76,53 @@ RSpec.describe Import::StatisticalFactory do
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
it 'tracks by has_many association attributes' do
Import::Test::GroupFactory.import([attributes])
# simulate next import run
travel 20.minutes
Import::Test::GroupFactory.reset_statistics
new_users = create_list(:user, 2)
attributes[:user_ids] = new_users.collect(&:id)
Import::Test::GroupFactory.import([attributes])
statistics = {
created: 0,
updated: 1,
unchanged: 0,
skipped: 0,
failed: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
it 'tracks by belongs_to association attributes' do
Import::Test::GroupFactory.import([attributes])
# simulate next import run
travel 20.minutes
Import::Test::GroupFactory.reset_statistics
new_signature = create(:signature)
attributes[:signature_id] = new_signature.id
Import::Test::GroupFactory.import([attributes])
statistics = {
created: 0,
updated: 1,
unchanged: 0,
skipped: 0,
failed: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
end
it 'tracks unchanged instances' do
Import::Test::GroupFactory.import([attributes])
@ -93,4 +143,112 @@ RSpec.describe Import::StatisticalFactory do
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
end
context 'dry run' do
it 'tracks created instances' do
Import::Test::GroupFactory.import([attributes], dry_run: true)
statistics = {
created: 1,
updated: 0,
unchanged: 0,
skipped: 0,
failed: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
context 'updated instances' do
let(:local_group) { create(:group) }
before(:each) do
ExternalSync.create(
source: 'RSpec-Test',
source_id: local_group.id,
object: 'Group',
o_id: local_group.id
)
end
it 'tracks by regular attributes' do
update_attributes = local_group.attributes
update_attributes[:note] = 'TEST'
Import::Test::GroupFactory.import([update_attributes], dry_run: true)
statistics = {
created: 0,
updated: 1,
unchanged: 0,
skipped: 0,
failed: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
it 'tracks by has_many association attributes' do
update_attributes = local_group.attributes
new_users = create_list(:user, 2)
update_attributes[:user_ids] = new_users.collect(&:id)
Import::Test::GroupFactory.import([update_attributes], dry_run: true)
statistics = {
created: 0,
updated: 1,
unchanged: 0,
skipped: 0,
failed: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
it 'tracks by belongs_to association attributes' do
update_attributes = local_group.attributes
new_signature = create(:signature)
update_attributes[:signature_id] = new_signature.id
Import::Test::GroupFactory.import([update_attributes], dry_run: true)
statistics = {
created: 0,
updated: 1,
unchanged: 0,
skipped: 0,
failed: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
end
it 'tracks unchanged instances' do
local_group = create(:group)
ExternalSync.create(
source: 'RSpec-Test',
source_id: local_group.id,
object: 'Group',
o_id: local_group.id
)
Import::Test::GroupFactory.import([local_group.attributes], dry_run: true)
statistics = {
created: 0,
updated: 0,
unchanged: 1,
skipped: 0,
failed: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
end
end
end