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:
parent
97114142ed
commit
9c07cecff5
5 changed files with 275 additions and 60 deletions
|
@ -5,11 +5,10 @@ module Import
|
||||||
attr_reader :resource, :remote_id, :errors
|
attr_reader :resource, :remote_id, :errors
|
||||||
|
|
||||||
def initialize(resource, *args)
|
def initialize(resource, *args)
|
||||||
|
@action = :unknown
|
||||||
handle_args(resource, *args)
|
handle_args(resource, *args)
|
||||||
initialize_associations_states
|
initialize_associations_states
|
||||||
import(resource, *args)
|
import(resource, *args)
|
||||||
return if @resource.blank?
|
|
||||||
store_associations(:after, @resource)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def import_class
|
def import_class
|
||||||
|
@ -26,10 +25,9 @@ module Import
|
||||||
|
|
||||||
def action
|
def action
|
||||||
return :failed if errors.present?
|
return :failed if errors.present?
|
||||||
return :skipped if !@resource
|
return :skipped if @resource.blank?
|
||||||
return :unchanged if !attributes_changed?
|
return :unchanged if !attributes_changed?
|
||||||
return :created if created?
|
@action
|
||||||
:updated
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def attributes_changed?
|
def attributes_changed?
|
||||||
|
@ -48,19 +46,12 @@ module Import
|
||||||
changes = {}
|
changes = {}
|
||||||
tracked_associations.each do |association|
|
tracked_associations.each do |association|
|
||||||
next if @associations[:before][association] == @associations[:after][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]]
|
changes[association] = [@associations[:before][association], @associations[:after][association]]
|
||||||
end
|
end
|
||||||
changes
|
changes
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def initialize_associations_states
|
def initialize_associations_states
|
||||||
|
@ -91,7 +82,14 @@ module Import
|
||||||
# the record is already created
|
# the record is already created
|
||||||
resource.delete(:created_by_id)
|
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
|
# the return value here is kind of misleading
|
||||||
# and should not be trusted to indicate if a
|
# and should not be trusted to indicate if a
|
||||||
|
@ -99,7 +97,10 @@ module Import
|
||||||
# Use .action instead
|
# Use .action instead
|
||||||
return true if !attributes_changed?
|
return true if !attributes_changed?
|
||||||
|
|
||||||
|
@action = :updated
|
||||||
|
|
||||||
return true if @dry_run
|
return true if @dry_run
|
||||||
|
@resource.assign_attributes(resource.slice(*associations))
|
||||||
@resource.save!
|
@resource.save!
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -119,18 +120,26 @@ module Import
|
||||||
instance
|
instance
|
||||||
end
|
end
|
||||||
|
|
||||||
def store_associations(state, instance)
|
def store_associations(state, source)
|
||||||
@associations[state] = associations_state(instance)
|
@associations[state] = associations_state(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
def associations_state(instance)
|
def associations_state(source)
|
||||||
state = {}
|
state = {}
|
||||||
tracked_associations.each do |association|
|
tracked_associations.each do |association|
|
||||||
state[association] = instance.send(association)
|
state[association] = association_value(source, association)
|
||||||
end
|
end
|
||||||
state
|
state
|
||||||
end
|
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
|
def tracked_associations
|
||||||
# loop over all reflections
|
# loop over all reflections
|
||||||
import_class.reflect_on_all_associations.collect do |reflection|
|
import_class.reflect_on_all_associations.collect do |reflection|
|
||||||
|
@ -150,6 +159,8 @@ module Import
|
||||||
|
|
||||||
def create(resource, *_args)
|
def create(resource, *_args)
|
||||||
@resource = import_class.new(resource)
|
@resource = import_class.new(resource)
|
||||||
|
store_associations(:after, @resource)
|
||||||
|
@action = :created
|
||||||
return if @dry_run
|
return if @dry_run
|
||||||
@resource.save!
|
@resource.save!
|
||||||
external_sync_create(
|
external_sync_create(
|
||||||
|
|
14
spec/factories/signature.rb
Normal file
14
spec/factories/signature.rb
Normal 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
|
|
@ -113,6 +113,38 @@ RSpec.describe Import::BaseResource do
|
||||||
ExternalSync.count
|
ExternalSync.count
|
||||||
}
|
}
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,7 @@ RSpec.describe Import::Ldap::User do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'logs failures to HTTP Log' do
|
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)
|
described_class.new(user_entry, ldap_config, user_roles, signup_role_ids)
|
||||||
|
|
||||||
expect(HttpLog.last.status).to eq('failed')
|
expect(HttpLog.last.status).to eq('failed')
|
||||||
|
@ -173,7 +173,7 @@ RSpec.describe Import::Ldap::User do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'logs failures to HTTP Log' do
|
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)
|
described_class.new(user_entry, ldap_config, user_roles, signup_role_ids)
|
||||||
|
|
||||||
expect(HttpLog.last.status).to eq('failed')
|
expect(HttpLog.last.status).to eq('failed')
|
||||||
|
|
|
@ -38,59 +38,217 @@ RSpec.describe Import::StatisticalFactory do
|
||||||
|
|
||||||
context 'statistics' do
|
context 'statistics' do
|
||||||
|
|
||||||
it 'tracks created instances' do
|
context 'live run' do
|
||||||
|
|
||||||
Import::Test::GroupFactory.import([attributes])
|
it 'tracks created instances' do
|
||||||
|
|
||||||
statistics = {
|
Import::Test::GroupFactory.import([attributes])
|
||||||
created: 1,
|
|
||||||
updated: 0,
|
statistics = {
|
||||||
unchanged: 0,
|
created: 1,
|
||||||
skipped: 0,
|
updated: 0,
|
||||||
failed: 0,
|
unchanged: 0,
|
||||||
}
|
skipped: 0,
|
||||||
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
|
failed: 0,
|
||||||
|
}
|
||||||
|
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'updated instances' do
|
||||||
|
it 'tracks by regular attributes' do
|
||||||
|
|
||||||
|
Import::Test::GroupFactory.import([attributes])
|
||||||
|
|
||||||
|
# simulate next import run
|
||||||
|
travel 20.minutes
|
||||||
|
Import::Test::GroupFactory.reset_statistics
|
||||||
|
|
||||||
|
attributes[:note] = 'TEST'
|
||||||
|
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 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])
|
||||||
|
|
||||||
|
# simulate next import run
|
||||||
|
travel 20.minutes
|
||||||
|
Import::Test::GroupFactory.reset_statistics
|
||||||
|
|
||||||
|
Import::Test::GroupFactory.import([attributes])
|
||||||
|
|
||||||
|
statistics = {
|
||||||
|
created: 0,
|
||||||
|
updated: 0,
|
||||||
|
unchanged: 1,
|
||||||
|
skipped: 0,
|
||||||
|
failed: 0,
|
||||||
|
}
|
||||||
|
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'tracks updated instances' do
|
context 'dry run' do
|
||||||
|
|
||||||
Import::Test::GroupFactory.import([attributes])
|
it 'tracks created instances' do
|
||||||
|
|
||||||
# simulate next import run
|
Import::Test::GroupFactory.import([attributes], dry_run: true)
|
||||||
travel 20.minutes
|
|
||||||
Import::Test::GroupFactory.reset_statistics
|
|
||||||
|
|
||||||
attributes[:note] = 'TEST'
|
statistics = {
|
||||||
Import::Test::GroupFactory.import([attributes])
|
created: 1,
|
||||||
|
updated: 0,
|
||||||
|
unchanged: 0,
|
||||||
|
skipped: 0,
|
||||||
|
failed: 0,
|
||||||
|
}
|
||||||
|
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
|
||||||
|
end
|
||||||
|
|
||||||
statistics = {
|
context 'updated instances' do
|
||||||
created: 0,
|
|
||||||
updated: 1,
|
|
||||||
unchanged: 0,
|
|
||||||
skipped: 0,
|
|
||||||
failed: 0,
|
|
||||||
}
|
|
||||||
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'tracks unchanged instances' do
|
let(:local_group) { create(:group) }
|
||||||
|
|
||||||
Import::Test::GroupFactory.import([attributes])
|
before(:each) do
|
||||||
|
ExternalSync.create(
|
||||||
|
source: 'RSpec-Test',
|
||||||
|
source_id: local_group.id,
|
||||||
|
object: 'Group',
|
||||||
|
o_id: local_group.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
# simulate next import run
|
it 'tracks by regular attributes' do
|
||||||
travel 20.minutes
|
|
||||||
Import::Test::GroupFactory.reset_statistics
|
|
||||||
|
|
||||||
Import::Test::GroupFactory.import([attributes])
|
update_attributes = local_group.attributes
|
||||||
|
update_attributes[:note] = 'TEST'
|
||||||
|
|
||||||
statistics = {
|
Import::Test::GroupFactory.import([update_attributes], dry_run: true)
|
||||||
created: 0,
|
|
||||||
updated: 0,
|
statistics = {
|
||||||
unchanged: 1,
|
created: 0,
|
||||||
skipped: 0,
|
updated: 1,
|
||||||
failed: 0,
|
unchanged: 0,
|
||||||
}
|
skipped: 0,
|
||||||
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue