From afe1cd4e8186784ad0e19ddb3e887d42881648be Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 20:55:33 +0200 Subject: [PATCH] test: cover OrmDirtyEncryptedTest and teardown --- test/test_orm_dirty_encrypted.rb | 110 +++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 test/test_orm_dirty_encrypted.rb diff --git a/test/test_orm_dirty_encrypted.rb b/test/test_orm_dirty_encrypted.rb new file mode 100644 index 0000000..b76b49f --- /dev/null +++ b/test/test_orm_dirty_encrypted.rb @@ -0,0 +1,110 @@ +require 'minitest/autorun' +require 'prism' +require 'tmpdir' + +# Session 55/56/58 left this as a known, unverified gap: Dirty (ActiveModel:: +# Dirty-lite) reads @attributes directly, so on an `encrypts`-backed column +# every dirty-tracking call (changed_attributes/changes/attribute_was/ +# attribute_change) leaked raw ciphertext instead of the plaintext the +# model's own reader returns. This file is the end-to-end proof that the +# fix (Dirty#humanize_dirty_value, Encryption's setter no-op-on-same-value) +# actually closes that gap, on the same non-deterministic and deterministic +# split test_orm_encryption.rb already exercises for plain reads. +class OrmDirtyEncryptedTest < Minitest::Test + def setup + @dir = Dir.mktmpdir('prism-dirty-encrypted-store') + @store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore')) + @store.create_table(:patients, %w[id name ssn notes]) + Prism::Orm::Encryption.primary_key = 'test-primary-key-do-not-use-in-prod' + + patient_class = Class.new(Prism::Orm::Model) do + table_name :patients + encrypts :ssn, deterministic: true + encrypts :notes + end + patient_class.store = @store + Object.const_set(:DirtyEncryptedPatient, patient_class) + end + + def teardown + FileUtils.remove_entry(@dir) + Prism::Orm::Encryption.primary_key = nil + Object.send(:remove_const, :DirtyEncryptedPatient) if Object.const_defined?(:DirtyEncryptedPatient) + end + + def test_new_record_reports_plaintext_not_ciphertext_in_changes + patient = DirtyEncryptedPatient.new(name: 'Alice', ssn: '123-45-6789', notes: 'note') + old_value, new_value = patient.changes['ssn'] + assert_nil old_value + assert_equal '123-45-6789', new_value + refute_includes patient.changes['ssn'].to_s, patient['ssn'].to_s + end + + def test_changed_attributes_reports_plaintext_old_value + patient = DirtyEncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'old note') + patient.notes = 'new note' + assert_equal 'old note', patient.changed_attributes['notes'] + end + + def test_attribute_was_and_attribute_change_report_plaintext_on_deterministic_column + patient = DirtyEncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'n') + patient.ssn = '999-99-9999' + assert_equal '123-45-6789', patient.attribute_was('ssn') + assert_equal ['123-45-6789', '999-99-9999'], patient.attribute_change('ssn') + end + + def test_attribute_was_and_attribute_change_report_plaintext_on_nondeterministic_column + patient = DirtyEncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'old note') + patient.notes = 'new note' + assert_equal 'old note', patient.attribute_was('notes') + assert_equal ['old note', 'new note'], patient.attribute_change('notes') + end + + def test_reassigning_the_same_plaintext_on_a_nondeterministic_column_is_not_dirty + patient = DirtyEncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'same note') + raw_before = patient['notes'] + patient.notes = 'same note' + refute patient.attribute_changed?('notes'), 'assigning the value already held must not be dirty' + refute patient.changed? + assert_equal raw_before, patient['notes'], 'a true no-op must not even rewrite the ciphertext' + end + + def test_reassigning_the_same_plaintext_on_a_deterministic_column_is_not_dirty + patient = DirtyEncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'n') + patient.ssn = '123-45-6789' + refute patient.attribute_changed?('ssn') + refute patient.changed? + end + + def test_saved_changes_after_create_reports_plaintext + patient = DirtyEncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'note') + assert_equal [nil, '123-45-6789'], patient.saved_changes['ssn'] + assert_equal [nil, 'note'], patient.saved_changes['notes'] + end + + def test_saved_changes_after_update_reports_plaintext_and_only_the_real_change + patient = DirtyEncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'old note') + patient.update!(notes: 'new note') + assert_equal ['old note', 'new note'], patient.saved_changes['notes'] + refute_includes patient.saved_changes.keys, 'ssn' + end + + def test_restore_attributes_still_reverts_the_underlying_ciphertext_correctly + patient = DirtyEncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'old note') + patient.notes = 'new note' + patient.restore_attributes(['notes']) + assert_equal 'old note', patient.notes + refute patient.changed? + end + + def test_a_corrupted_current_value_does_not_block_overwriting_it + patient = DirtyEncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'note') + row = @store.find(:patients, patient.id) + tampered = row.dup + tampered['notes'] = row['notes'].sub(/^./) { |c| c == 'A' ? 'B' : 'A' } + @store.update(:patients, patient.id, tampered) + reloaded = DirtyEncryptedPatient.find(patient.id) + reloaded.notes = 'fresh note' + assert_equal 'fresh note', reloaded.notes + end +end