# frozen_string_literal: true require 'spec_helper' require 'securerandom' describe Aws::Record::DirtyTracking do let(:klass) do Class.new do include(Aws::Record) set_table_name(:test_table) string_attr(:mykey, hash_key: true) string_attr(:body) end end let(:instance) { klass.new } let(:stub_client) { Aws::DynamoDB::Client.new(stub_responses: true) } describe '#[attribute]_dirty?' do it 'should return whether the attribute is dirty or clean' do expect(instance.mykey_dirty?).to be false instance.mykey = SecureRandom.uuid expect(instance.mykey_dirty?).to be true end it 'should not reflect changes to the original value as dirty' do instance.mykey = nil expect(instance.mykey_dirty?).to be false instance.mykey = SecureRandom.uuid expect(instance.mykey_dirty?).to be true instance.mykey = nil expect(instance.mykey_dirty?).to be false end it 'should recognize initialization values as dirty' do item = klass.new(mykey: 'Key', body: 'Hello!') expect(item.mykey_dirty?).to be_truthy end end describe '#[attribute]_dirty!' do before(:each) do instance.mykey = 'Alex' instance.clean! end it 'should mark the attribute as dirty' do expect(instance.mykey_dirty?).to be false instance.mykey_dirty! expect(instance.mykey_dirty?).to be true instance.mykey = 's' expect(instance.mykey_dirty?).to be true end it 'should take a snapshot of the attribute' do expect(instance.mykey_was).to eq 'Alex' expect(instance.mykey).to eq 'Alex' instance.mykey = 'Alexi' expect(instance.mykey_was).to eq 'Alex' expect(instance.mykey).to eq 'Alexi' instance.mykey_dirty! expect(instance.mykey_was).to eq 'Alex' expect(instance.mykey).to eq 'Alexi' instance.mykey = 'Alexis' expect(instance.mykey_was).to eq 'Alex' expect(instance.mykey).to eq 'Alexis' end end describe '#[attribute]_was' do it 'should return the last known clean value' do expect(instance.mykey_was).to be nil instance.mykey = SecureRandom.uuid expect(instance.mykey_was).to be nil end end describe '#clean!' do it 'should mark the record as clean' do instance.mykey = SecureRandom.uuid expect(instance.dirty?).to be true expect(instance.mykey_was).to be nil instance.clean! expect(instance.dirty?).to be false expect(instance.mykey_was).to eq instance.mykey end end describe '#dirty' do it 'should return an array of dirty attributes' do expect(instance.dirty).to match_array [] instance.mykey = SecureRandom.uuid expect(instance.dirty).to match_array [:mykey] instance.body = SecureRandom.uuid expect(instance.dirty).to match_array %i[mykey body] end end describe '#dirty?' do it 'should return whether the record is dirty or clean' do expect(instance.dirty?).to be false instance.mykey = SecureRandom.uuid expect(instance.dirty?).to be true end end describe '#reload!' do before(:each) do klass.configure_client(client: stub_client) end let(:reloaded_instance) do item = klass.new item.mykey = SecureRandom.uuid item.body = SecureRandom.uuid item.clean! item end it 'can reload an item using find' do expect(klass) .to receive(:find) .with({ mykey: reloaded_instance.mykey }) # rubocop:disable Style/BracesAroundHashParameters .and_return(reloaded_instance) instance.mykey = reloaded_instance.mykey instance.body = SecureRandom.uuid instance.reload! expect(instance.body).to eq reloaded_instance.body end it 'raises an error when find returns nil' do instance.mykey = SecureRandom.uuid expect(klass) .to receive(:find) .with({ mykey: instance.mykey }) # rubocop:disable Style/BracesAroundHashParameters .and_return(nil) expect { instance.reload! }.to raise_error Aws::Record::Errors::NotFound end it 'should mark the item as clean' do instance.mykey = SecureRandom.uuid expect(instance.dirty?).to be true instance.reload! expect(instance.dirty?).to be false end end describe 'persisted?' do before(:each) do klass.configure_client(client: stub_client) end it 'appropriately determines whether an item is persisted' do item = klass.new item.mykey = 'mykey' item.body = 'body' # Test all combinations of new_recorded and destroyed expect(item.persisted?).to be false item.save expect(item.persisted?).to be true item.delete! expect(item.persisted?).to be false item = klass.new item.mykey = 'mykey' item.body = 'body' item.delete! expect(item.persisted?).to be false end end describe 'persisted? with ActiveModel::Model' do let(:klass) do Class.new do include(ActiveModel::Model) include(Aws::Record) set_table_name(:test_table) string_attr(:mykey, hash_key: true) string_attr(:body) end end before(:each) do klass.configure_client(client: stub_client) end it 'appropriately determines whether an item is persisted' do item = klass.new item.mykey = 'mykey' item.body = 'body' # Test all combinations of new_recorded and destroyed expect(item.persisted?).to be false item.save expect(item.persisted?).to be true item.delete! expect(item.persisted?).to be false item = klass.new item.mykey = 'mykey' item.body = 'body' item.delete! expect(item.persisted?).to be false end end describe '#rollback_[attribute]!' do it 'should restore the attribute to its last known clean value' do original_mykey = instance.mykey instance.mykey = SecureRandom.uuid instance.rollback_mykey! expect(instance.mykey).to be original_mykey end end describe '#rollback!' do it 'should restore the provided attributes' do original_mykey = instance.mykey instance.mykey = SecureRandom.uuid instance.body = updated_body = SecureRandom.uuid instance.rollback!(:mykey) expect(instance.mykey).to eq original_mykey expect(instance.body).to eq updated_body end context 'when no attributes are provided' do it 'should restore all attributes' do original_mykey = instance.mykey original_body = instance.body instance.mykey = SecureRandom.uuid instance.body = SecureRandom.uuid instance.rollback! expect(instance.dirty?).to be false expect(instance.mykey).to eq original_mykey expect(instance.body).to eq original_body end end end describe '#update' do before(:each) do klass.configure_client(client: stub_client) end it 'assign_attributes should perform a hash based attribute assignment without persisting changes' do item = klass.new item.mykey = 'mykey' item.body = 'body' item.save new_key = 'newkey' new_body = 'newbody' item.assign_attributes(mykey: new_key, body: new_body) expect(item.mykey).to eq new_key expect(item.body).to eq new_body expect(item.dirty?).to be true end it 'update should perform a hash based attribute assignment and persist changes' do item = klass.new item.mykey = 'mykey' item.body = 'body' item.save new_key = 'newkey' new_body = 'newbody' item.update(mykey: new_key, body: new_body) expect(item.mykey).to eq new_key expect(item.body).to eq new_body expect(item.dirty?).to be false end it 'automatically tokenizes update hash keys' do item = klass.new item.mykey = 'mykey' item.body = 'body' item.save new_key = 'newkey' new_body = 'newbody' item.update(mykey: new_key, body: new_body) expect(item.mykey).to eq new_key expect(item.body).to eq new_body expect(item.dirty?).to be false end it 'update not update when an invalid update is performed' do model = Object.const_set( 'TestUpdateValidationModel', Class.new do include(Aws::Record) include(ActiveModel::Validations) set_table_name('TestTable') integer_attr(:id, hash_key: true) string_attr(:body) validates_length_of(:body, maximum: 5) end ) model.configure_client(client: stub_client) record = model.new(id: 1, body: '12345') record.save res = record.update(body: '123456') expect(res).to be(false) end it 'update! should throw an error when a validation error occurs' do model = Class.new do include(Aws::Record) include(ActiveModel::Validations) set_table_name('TestTable') integer_attr(:id, hash_key: true) string_attr(:body) validates_length_of(:body, maximum: 5) end model.configure_client(client: stub_client) record = model.new(id: 1, body: '12345') record.save expect { record.update!(body: '123456').to raise_error(Errors::ValidationError) } end it 'should throw an argument error when you try to update an invalid attribute' do item = klass.new item.mykey = 'mykey' item.body = 'body' item.save expect { item.assign_attributes(mykey_key: 'ThrowsError') }.to raise_error(ArgumentError) end end describe '#update with ActiveModel::Model' do let(:klass) do Class.new do include(ActiveModel::Model) include(Aws::Record) set_table_name(:test_table) string_attr(:mykey, hash_key: true) string_attr(:body) end end before(:each) do klass.configure_client(client: stub_client) end it 'assign_attributes should perform a hash based attribute assignment without persisting changes' do item = klass.new item.mykey = 'mykey' item.body = 'body' item.save new_key = 'newkey' new_body = 'newbody' item.assign_attributes(mykey: new_key, body: new_body) expect(item.mykey).to eq new_key expect(item.body).to eq new_body expect(item.dirty?).to be true end it 'update should perform a hash based attribute assignment and persist changes' do item = klass.new item.mykey = 'mykey' item.body = 'body' item.save new_key = 'newkey' new_body = 'newbody' item.update(mykey: new_key, body: new_body) expect(item.mykey).to eq new_key expect(item.body).to eq new_body expect(item.dirty?).to be false end it 'automatically tokenizes update hash keys' do item = klass.new item.mykey = 'mykey' item.body = 'body' item.save new_key = 'newkey' new_body = 'newbody' item.update(:mykey => new_key, 'body' => new_body) expect(item.mykey).to eq new_key expect(item.body).to eq new_body expect(item.dirty?).to be false end it 'update! should throw an error when a validation error occurs' do model = Class.new do include(Aws::Record) include(ActiveModel::Validations) set_table_name('TestTable') integer_attr(:id, hash_key: true) string_attr(:body) validates_length_of(:body, maximum: 5) end model.configure_client(client: stub_client) record = model.new(id: 1, body: '12345') record.save expect { record.update!(body: '123456').to raise_error(Errors::ValidationError) } end it 'should throw an argument error when you try to update an invalid attribute' do item = klass.new item.mykey = 'mykey' item.body = 'body' item.save expect { item.assign_attributes(mykey_key: 'ThrowsError') }.to raise_error(ArgumentError) end end describe '#save' do before(:each) do klass.configure_client(client: stub_client) end it 'should mark the item as clean' do instance.mykey = SecureRandom.uuid expect(instance.dirty?).to be true instance.save expect(instance.dirty?).to be false end end describe '#find' do before(:each) do klass.configure_client(client: stub_client) end it 'should mark the item as clean' do found_item = klass.find(mykey: 1) expect(found_item.dirty?).to be false end end describe 'Mutation Dirty Tracking' do let(:klass) do Class.new do include(Aws::Record) set_table_name(:test_table) string_attr(:mykey, hash_key: true) string_attr(:body) list_attr(:dirty_list) map_attr(:dirty_map) string_set_attr(:dirty_set) end end describe 'Default Values' do let(:klass_with_defaults) do Class.new do include(Aws::Record) set_table_name(:test_table) string_attr(:mykey, hash_key: true) map_attr(:dirty_map, default_value: {}) end end it 'tracks mutations to the default value' do item = klass_with_defaults.new(mykey: 'key') item.clean! expect(item.dirty?).to be_falsy item.dirty_map[:key] = 'value' expect(item.dirty_map).to eq(key: 'value') expect(item.dirty?).to be_truthy end end describe 'Tracking Turned Off' do it 'does not track detailed mutations when tracking is globally off' do klass.disable_mutation_tracking item = klass.new(mykey: '1', dirty_list: [1, 2, 3]) item.clean! item.dirty_list << 4 expect(item.dirty_list).to eq([1, 2, 3, 4]) expect(item.dirty?).to be_falsy end end describe 'Lists' do it 'marks mutated lists as dirty' do item = klass.new(mykey: '1', dirty_list: [1, 2, 3]) item.clean! item.dirty_list << 4 expect(item.dirty_list).to eq([1, 2, 3, 4]) expect(item.dirty?).to be_truthy expect(item.attribute_dirty?(:dirty_list)).to be_truthy end it 'has a copy of the mutated list to reference and can roll back' do item = klass.new(mykey: '1', dirty_list: [1, 2, 3]) item.clean! item.dirty_list << 4 expect(item.dirty_list_was).to eq([1, 2, 3]) item.rollback!(:dirty_list) expect(item.dirty_list).to eq([1, 2, 3]) end it 'includes the mutated list in the list of dirty attributes' do item = klass.new(mykey: '1', body: 'b', dirty_list: [1, 2, 3]) item.clean! item.body = 'body' item.dirty_list << 4 expect(item.dirty).to eq(%i[body dirty_list]) end it 'correctly unmarks attributes as dirty when rolling back from copy' do item = klass.new(mykey: '1', dirty_list: [1, 2, 3]) item.clean! item.attribute_dirty!(:dirty_list) expect(item.dirty).to eq([:dirty_list]) item.dirty_list << 4 expect(item.dirty).to eq([:dirty_list]) item.rollback_attribute!(:dirty_list) expect(item.dirty?).to be_falsy end it 'correctly handles #clean! with a mutated list' do item = klass.new(mykey: '1', body: 'b', dirty_list: [1, 2, 3]) item.clean! item.dirty_list << 4 expect(item.dirty?).to be_truthy item.clean! expect(item.dirty?).to be_falsy expect(item.attribute_was(:dirty_list)).to eq([1, 2, 3, 4]) end it 'correctly handles nested mutated lists' do my_list = [[1], [1, 2], [1, 2, 3]] item = klass.new(mykey: '1', dirty_list: my_list) item.clean! expect(item.dirty?).to be_falsy my_list[0] << 2 my_list[1] << 3 my_list[2] << 4 expect(item.dirty_list).to eq([[1, 2], [1, 2, 3], [1, 2, 3, 4]]) expect(item.dirty_list_was).to eq([[1], [1, 2], [1, 2, 3]]) expect(item.dirty?).to be_truthy item.rollback_attribute!(:dirty_list) expect(item.dirty_list).to eq([[1], [1, 2], [1, 2, 3]]) end it 'correctly handles list equality through assignment' do item = klass.new(mykey: '1', dirty_list: [1, 2, 3]) item.clean! item.dirty_list << 4 expect(item.dirty?).to be_truthy item.dirty_list = [1, 2, 3] expect(item.dirty?).to be_falsy end end describe 'Maps' do it 'marks mutated maps as dirty' do item = klass.new(mykey: '1', dirty_map: { a: 1, b: '2' }) item.clean! item.dirty_map[:c] = 3.0 expect(item.dirty_map).to eq(a: 1, b: '2', c: 3.0) expect(item.dirty?).to be_truthy expect(item.attribute_dirty?(:dirty_map)).to be_truthy end it 'has a copy of the mutated map to reference and can roll back' do item = klass.new(mykey: '1', dirty_map: { a: 1, b: '2' }) item.clean! item.dirty_map[:c] = 3.0 expect(item.dirty_map_was).to eq(a: 1, b: '2') item.rollback!(:dirty_map) expect(item.dirty_map).to eq(a: 1, b: '2') end it 'includes the mutated map in the list of dirty attributes' do item = klass.new(mykey: '1', body: 'b', dirty_map: { a: 1, b: '2' }) item.clean! item.body = 'body' item.dirty_map[:c] = 3.0 expect(item.dirty).to eq(%i[body dirty_map]) end it 'correctly unmarks attributes as dirty when rolling back from copy' do item = klass.new(mykey: '1', dirty_map: { a: 1, b: '2' }) item.clean! item.attribute_dirty!(:dirty_map) expect(item.dirty).to eq([:dirty_map]) item.dirty_map[:c] = 3.0 expect(item.dirty).to eq([:dirty_map]) item.rollback_attribute!(:dirty_map) expect(item.dirty?).to be_falsy end it 'correctly handles #clean! with a mutated map' do item = klass.new(mykey: '1', dirty_map: { a: 1, b: '2' }) item.clean! item.dirty_map[:c] = 3.0 expect(item.dirty?).to be_truthy item.clean! expect(item.dirty?).to be_falsy expect(item.attribute_was(:dirty_map)).to eq(a: 1, b: '2', c: 3.0) end it 'correctly handles nested mutated maps' do my_map = { a: { one: 1, two: 2.0 }, b: 2 } item = klass.new(mykey: '1', dirty_map: my_map) item.clean! expect(item.dirty?).to be_falsy my_map[:a][:three] = '3' my_map[:c] = { nesting: true } expect(item.dirty_map).to eq( a: { one: 1, two: 2.0, three: '3' }, b: 2, c: { nesting: true } ) expect(item.dirty_map_was).to eq( a: { one: 1, two: 2.0 }, b: 2 ) expect(item.dirty?).to be_truthy item.rollback_attribute!(:dirty_map) expect(item.dirty_map).to eq( a: { one: 1, two: 2.0 }, b: 2 ) end it 'correctly handles map equality through assignment' do item = klass.new(mykey: '1', dirty_map: { a: 1, b: '2' }) item.clean! item.dirty_map[:c] = 3.0 expect(item.dirty?).to be_truthy item.dirty_map = { a: 1, b: '2' } expect(item.dirty?).to be_falsy end end describe 'Sets' do it 'marks mutated sets as dirty' do item = klass.new(mykey: '1', dirty_set: Set.new(%w[a b c])) item.clean! item.dirty_set.add('d') expect(item.dirty_set).to eq(Set.new(%w[a b c d])) expect(item.dirty?).to be_truthy expect(item.attribute_dirty?(:dirty_set)).to be_truthy end it 'has a copy of the mutated set to reference and can roll back' do item = klass.new(mykey: '1', dirty_set: Set.new(%w[a b c])) item.clean! item.dirty_set.add('d') expect(item.dirty_set_was).to eq(Set.new(%w[a b c])) item.rollback!(:dirty_set) expect(item.dirty_set).to eq(Set.new(%w[a b c])) end it 'includes the mutated set in the list of dirty attributes' do item = klass.new(mykey: '1', body: 'b', dirty_set: Set.new(%w[a b c])) item.clean! item.body = 'body' item.dirty_set.add('d') expect(item.dirty).to eq(%i[body dirty_set]) end it 'correctly unmarks attributes as dirty when rolling back from copy' do item = klass.new(mykey: '1', dirty_set: Set.new(%w[a b c])) item.clean! item.attribute_dirty!(:dirty_set) expect(item.dirty).to eq([:dirty_set]) item.dirty_set.add('d') expect(item.dirty).to eq([:dirty_set]) item.rollback_attribute!(:dirty_set) expect(item.dirty?).to be_falsy end it 'correctly handles #clean! with a mutated set' do item = klass.new(mykey: '1', dirty_set: Set.new(%w[a b c])) item.clean! item.dirty_set.add('d') expect(item.dirty?).to be_truthy item.clean! expect(item.dirty?).to be_falsy expect(item.attribute_was(:dirty_set)).to eq(Set.new(%w[a b c d])) end it 'correctly handles set equality through assignment' do item = klass.new(mykey: '1', dirty_set: Set.new(%w[a b c])) item.clean! item.dirty_set.add('d') expect(item.dirty?).to be_truthy item.dirty_set = Set.new(%w[a b c]) expect(item.dirty?).to be_falsy end end end end