Fixed issue #1663 - Sequencer ExternalSync lookup and storage is affected by ID collisions.

This commit is contained in:
Thorsten Eckel 2018-01-10 11:55:02 +01:00
parent 0ea8dd7c45
commit 50215b0b8e
8 changed files with 160 additions and 22 deletions

View file

@ -5,6 +5,10 @@ class ExternalSync < ApplicationModel
class << self class << self
def sanitized_source_id(source_id)
Digest::SHA2.hexdigest(source_id)
end
def changed?(object:, previous_changes: {}, current_changes:) def changed?(object:, previous_changes: {}, current_changes:)
changed = false changed = false
previous_changes ||= {} previous_changes ||= {}

View file

@ -12,7 +12,7 @@ class Sequencer
def process def process
state.provide(:remote_id) do state.provide(:remote_id) do
resource.fetch(attribute).dup.to_s.downcase resource.fetch(attribute).dup.to_s
end end
rescue KeyError => e rescue KeyError => e
handle_failure(e) handle_failure(e)

View file

@ -19,8 +19,8 @@ class Sequencer
def up_to_date? def up_to_date?
return false if entry.blank? return false if entry.blank?
return true if entry.source_id == remote_id return true if entry.source_id == sanitized_remote_id
entry.update!(source_id: remote_id) entry.update!(source_id: sanitized_remote_id)
true true
end end
@ -37,11 +37,15 @@ class Sequencer
def create def create
::ExternalSync.create( ::ExternalSync.create(
source: external_sync_source, source: external_sync_source,
source_id: remote_id, source_id: sanitized_remote_id,
object: model_class.name, object: model_class.name,
o_id: instance.id o_id: instance.id
) )
end end
def sanitized_remote_id
@sanitized_remote_id ||= ::ExternalSync.sanitized_source_id(remote_id)
end
end end
end end
end end

View file

@ -14,11 +14,6 @@ class Sequencer
provides :instance provides :instance
def process def process
synced_instance = ::ExternalSync.find_by(
source: external_sync_source,
source_id: remote_id,
object: model_class.name,
)
return if !synced_instance return if !synced_instance
state.provide(:instance) do state.provide(:instance) do
@ -27,6 +22,55 @@ class Sequencer
rescue => e rescue => e
handle_failure(e) handle_failure(e)
end end
private
def synced_instance
@synced_instance ||= correct_entry || corrected_entry
end
def correct_entry
::ExternalSync.find_by(
source: external_sync_source,
source_id: sanitized_remote_id,
object: model_class.name,
)
end
def sanitized_remote_id
@sanitized_remote_id ||= ::ExternalSync.sanitized_source_id(remote_id)
end
def corrected_entry
return if obsolete_entry.blank?
obsolete_entry.update!(source_id: sanitized_remote_id)
obsolete_entry
end
def obsolete_entry
@obsolete_entry ||= begin
if Rails.application.config.db_case_sensitive
case_sensitive_entry
else
case_insensitive_entry
end
end
end
def case_sensitive_entry
::ExternalSync.where(
source: external_sync_source,
object: model_class.name,
).where('LOWER(source_id) = LOWER(?)', remote_id).first
end
def case_insensitive_entry
::ExternalSync.find_by(
source: external_sync_source,
source_id: remote_id,
object: model_class.name,
)
end
end end
end end
end end

View file

@ -13,7 +13,7 @@ class Sequencer
end end
def active_ids def active_ids
ExternalSync.joins('INNER JOIN users ON (users.id = external_syncs.o_id)') ::ExternalSync.joins('INNER JOIN users ON (users.id = external_syncs.o_id)')
.where( .where(
source: external_sync_source, source: external_sync_source,
object: model_class.name, object: model_class.name,

View file

@ -2,6 +2,27 @@ require 'rails_helper'
RSpec.describe ExternalSync do RSpec.describe ExternalSync do
context '#sanitized_source_id' do
let(:source_id) { 'AbCdEfG124' }
it 'sanitizes source ids' do
sanitized_source_id = described_class.sanitized_source_id(source_id)
expect(sanitized_source_id).to_not eq(source_id)
end
it 'returns case insenstive value' do
sanitized_source_id = described_class.sanitized_source_id(source_id)
expect(sanitized_source_id).to eq(sanitized_source_id.downcase)
end
it 'avoids case sensitive collitions' do
sanitized_source_id = described_class.sanitized_source_id(source_id)
sanitized_source_id_downcased = described_class.sanitized_source_id(source_id.downcase)
expect(sanitized_source_id).to_not eq(sanitized_source_id_downcased)
end
end
context '#changed?' do context '#changed?' do
it 'keeps ActiveRecord instance unchanged on local but no remote changes' do it 'keeps ActiveRecord instance unchanged on local but no remote changes' do

View file

@ -38,9 +38,10 @@ RSpec.describe Sequencer::Unit::Import::Common::Model::Attributes::RemoteId, seq
provided = process(parameters) provided = process(parameters)
expect(provided).to include(remote_id: '1337') expect(provided).to include(remote_id: '1337')
expect(provided[:remote_id]).to be_a(String)
end end
it 'downcases the value to prevent case sensivity issues with the ORM' do it 'does not change the value to prevent id collision issues' do
parameters = { parameters = {
resource: { resource: {
id: 'AbCdEfG', id: 'AbCdEfG',
@ -49,7 +50,7 @@ RSpec.describe Sequencer::Unit::Import::Common::Model::Attributes::RemoteId, seq
provided = process(parameters) provided = process(parameters)
expect(provided[:remote_id]).to eq(parameters[:resource][:id].downcase) expect(provided[:remote_id]).to eq(parameters[:resource][:id])
end end
it 'duplicates the value to prevent attribute changes' do it 'duplicates the value to prevent attribute changes' do

View file

@ -2,11 +2,34 @@ require 'rails_helper'
RSpec.describe Sequencer::Unit::Import::Common::Model::Lookup::ExternalSync, sequencer: :unit do RSpec.describe Sequencer::Unit::Import::Common::Model::Lookup::ExternalSync, sequencer: :unit do
it 'finds model_class instances by remote_id' do it 'finds model_class instance by remote_id' do
user = create(:user) user = create(:user)
external_sync_source = 'test' external_sync_source = 'test'
remote_id = '1337' remote_id = '1337'
ExternalSync.create(
source: external_sync_source,
source_id: ExternalSync.sanitized_source_id(remote_id),
o_id: user.id,
object: user.class,
)
provided = process(
remote_id: remote_id,
model_class: user.class,
external_sync_source: external_sync_source,
)
expect(provided[:instance]).to eq(user)
end
context 'obsolete plain remote_id' do
let(:user) { create(:user) }
let(:external_sync_source) { 'test' }
let(:remote_id) { 'AbCdEfG' }
it 'finds model_class instance' do
ExternalSync.create( ExternalSync.create(
source: external_sync_source, source: external_sync_source,
source_id: remote_id, source_id: remote_id,
@ -22,4 +45,45 @@ RSpec.describe Sequencer::Unit::Import::Common::Model::Lookup::ExternalSync, seq
expect(provided[:instance]).to eq(user) expect(provided[:instance]).to eq(user)
end end
it 'corrects external sync entry' do
entry = ExternalSync.create(
source: external_sync_source,
source_id: remote_id,
o_id: user.id,
object: user.class,
)
process(
remote_id: remote_id,
model_class: user.class,
external_sync_source: external_sync_source,
)
entry.reload
expect(entry.source_id).to eq(ExternalSync.sanitized_source_id(remote_id))
end
it 'operates case agnostic' do
entry = ExternalSync.create(
source: external_sync_source,
source_id: remote_id.downcase,
o_id: user.id,
object: user.class,
)
provided = process(
remote_id: remote_id,
model_class: user.class,
external_sync_source: external_sync_source,
)
expect(provided[:instance]).to eq(user)
entry.reload
expect(entry.source_id).to eq(ExternalSync.sanitized_source_id(remote_id))
end
end
end end