familia 2.0.0.pre3 → 2.0.0.pre5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +3 -3
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +18 -3
  7. data/README.md +36 -157
  8. data/TEST_COVERAGE.md +40 -0
  9. data/docs/overview.md +359 -0
  10. data/docs/wiki/API-Reference.md +270 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
  12. data/docs/wiki/Home.md +49 -0
  13. data/docs/wiki/Implementation-Guide.md +183 -0
  14. data/docs/wiki/Security-Model.md +143 -0
  15. data/lib/familia/base.rb +18 -27
  16. data/lib/familia/connection.rb +6 -5
  17. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  18. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  19. data/lib/familia/{datatype → data_type}/types/hashkey.rb +2 -2
  20. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  21. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  22. data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
  23. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  24. data/lib/familia/{datatype.rb → data_type.rb} +10 -12
  25. data/lib/familia/encryption/manager.rb +102 -0
  26. data/lib/familia/encryption/provider.rb +49 -0
  27. data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
  28. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  29. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -0
  30. data/lib/familia/encryption/registry.rb +50 -0
  31. data/lib/familia/encryption.rb +178 -0
  32. data/lib/familia/encryption_request_cache.rb +68 -0
  33. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +153 -0
  34. data/lib/familia/features/encrypted_fields.rb +28 -0
  35. data/lib/familia/features/expiration.rb +107 -77
  36. data/lib/familia/features/quantization.rb +5 -9
  37. data/lib/familia/features/relatable_objects.rb +2 -4
  38. data/lib/familia/features/safe_dump.rb +14 -17
  39. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  40. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  41. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  42. data/lib/familia/features/transient_fields.rb +47 -0
  43. data/lib/familia/features.rb +40 -24
  44. data/lib/familia/field_type.rb +270 -0
  45. data/lib/familia/horreum/connection.rb +8 -11
  46. data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
  47. data/lib/familia/horreum/definition_methods.rb +453 -0
  48. data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
  49. data/lib/familia/horreum/serialization.rb +46 -18
  50. data/lib/familia/horreum/settings.rb +10 -2
  51. data/lib/familia/horreum/utils.rb +9 -10
  52. data/lib/familia/horreum.rb +18 -10
  53. data/lib/familia/logging.rb +14 -14
  54. data/lib/familia/settings.rb +39 -3
  55. data/lib/familia/utils.rb +45 -0
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -1
  58. data/try/core/base_enhancements_try.rb +115 -0
  59. data/try/core/connection_try.rb +0 -1
  60. data/try/core/errors_try.rb +0 -1
  61. data/try/core/familia_extended_try.rb +3 -4
  62. data/try/core/familia_try.rb +0 -1
  63. data/try/core/pools_try.rb +2 -2
  64. data/try/core/secure_identifier_try.rb +0 -1
  65. data/try/core/settings_try.rb +0 -1
  66. data/try/core/utils_try.rb +0 -1
  67. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  68. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  69. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  70. data/try/{datatypes → data_types}/list_try.rb +1 -2
  71. data/try/{datatypes → data_types}/set_try.rb +1 -2
  72. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  73. data/try/{datatypes → data_types}/string_try.rb +1 -2
  74. data/try/debugging/README.md +32 -0
  75. data/try/debugging/cache_behavior_tracer.rb +91 -0
  76. data/try/debugging/encryption_method_tracer.rb +138 -0
  77. data/try/debugging/provider_diagnostics.rb +110 -0
  78. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  79. data/try/edge_cases/json_serialization_try.rb +0 -1
  80. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  81. data/try/encryption/config_persistence_try.rb +192 -0
  82. data/try/encryption/encryption_core_try.rb +328 -0
  83. data/try/encryption/instance_variable_scope_try.rb +31 -0
  84. data/try/encryption/module_loading_try.rb +28 -0
  85. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  86. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  87. data/try/encryption/roundtrip_validation_try.rb +28 -0
  88. data/try/encryption/secure_memory_handling_try.rb +125 -0
  89. data/try/features/encrypted_fields_core_try.rb +117 -0
  90. data/try/features/encrypted_fields_integration_try.rb +220 -0
  91. data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
  92. data/try/features/encrypted_fields_security_try.rb +370 -0
  93. data/try/features/encryption_fields/aad_protection_try.rb +53 -0
  94. data/try/features/encryption_fields/context_isolation_try.rb +120 -0
  95. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  96. data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
  97. data/try/features/encryption_fields/fresh_key_try.rb +163 -0
  98. data/try/features/encryption_fields/key_rotation_try.rb +117 -0
  99. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  100. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  101. data/try/features/encryption_fields/nonce_uniqueness_try.rb +54 -0
  102. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  103. data/try/features/expiration_try.rb +0 -1
  104. data/try/features/feature_dependencies_try.rb +159 -0
  105. data/try/features/quantization_try.rb +0 -1
  106. data/try/features/real_feature_integration_try.rb +148 -0
  107. data/try/features/relatable_objects_try.rb +0 -1
  108. data/try/features/safe_dump_advanced_try.rb +0 -1
  109. data/try/features/safe_dump_try.rb +0 -1
  110. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  111. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  112. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  113. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  114. data/try/features/transient_fields_core_try.rb +181 -0
  115. data/try/features/transient_fields_integration_try.rb +260 -0
  116. data/try/helpers/test_helpers.rb +42 -0
  117. data/try/horreum/base_try.rb +157 -3
  118. data/try/horreum/class_methods_try.rb +27 -36
  119. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  120. data/try/horreum/field_categories_try.rb +118 -0
  121. data/try/horreum/field_definition_try.rb +96 -0
  122. data/try/horreum/initialization_try.rb +0 -1
  123. data/try/horreum/relations_try.rb +0 -1
  124. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  125. data/try/horreum/serialization_try.rb +2 -3
  126. data/try/memory/memory_basic_test.rb +73 -0
  127. data/try/memory/memory_detailed_test.rb +121 -0
  128. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  129. data/try/memory/memory_search_for_string.rb +83 -0
  130. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  131. data/try/models/customer_safe_dump_try.rb +0 -1
  132. data/try/models/customer_try.rb +0 -1
  133. data/try/models/datatype_base_try.rb +1 -2
  134. data/try/models/familia_object_try.rb +0 -1
  135. metadata +85 -18
@@ -0,0 +1,248 @@
1
+ # try/features/transient_fields/redacted_string_try.rb
2
+
3
+ require_relative '../../helpers/test_helpers'
4
+
5
+
6
+ # Create sample sensitive values for testing
7
+ @api_key = "sk-1234567890abcdef"
8
+ @password = "super_secret_password_123!"
9
+ @empty_secret = ""
10
+ @long_secret = "a" * 100 # Test long string handling
11
+ @special_chars = "päßwörd!@#$%^&*()"
12
+
13
+ ## TEST CASES
14
+
15
+ ## Basic initialization creates RedactedString instance
16
+ redacted = RedactedString.new(@api_key)
17
+ redacted.class
18
+ #=> RedactedString
19
+
20
+ ## Initialization accepts various input types
21
+ RedactedString.new("string").class
22
+ #=> RedactedString
23
+
24
+ RedactedString.new(123).class # to_s conversion
25
+ #=> RedactedString
26
+
27
+ RedactedString.new(nil).class # nil handling
28
+ #=> RedactedString
29
+
30
+ ## Empty string handling
31
+ empty_redacted = RedactedString.new(@empty_secret)
32
+ empty_redacted.class
33
+ #=> RedactedString
34
+
35
+ ## Long string handling
36
+ long_redacted = RedactedString.new(@long_secret)
37
+ long_redacted.class
38
+ #=> RedactedString
39
+
40
+ ## Special characters handling
41
+ special_redacted = RedactedString.new(@special_chars)
42
+ special_redacted.class
43
+ #=> RedactedString
44
+
45
+ ## Fresh instance is not cleared initially
46
+ fresh_redacted = RedactedString.new(@api_key)
47
+ fresh_redacted.cleared?
48
+ #=> false
49
+
50
+ ## to_s always returns redacted placeholder
51
+ redacted_for_to_s = RedactedString.new(@api_key)
52
+ redacted_for_to_s.to_s
53
+ #=> "[REDACTED]"
54
+
55
+ ## inspect returns same as to_s for security
56
+ redacted_for_inspect = RedactedString.new(@password)
57
+ redacted_for_inspect.inspect
58
+ #=> "[REDACTED]"
59
+
60
+ ## String interpolation is redacted
61
+ redacted_for_interpolation = RedactedString.new(@api_key)
62
+ "Token: #{redacted_for_interpolation}"
63
+ #=> "Token: [REDACTED]"
64
+
65
+ ## Array/Hash containing redacted strings show redacted values
66
+ redacted_in_array = RedactedString.new(@password)
67
+ [redacted_in_array].to_s.include?("[REDACTED]")
68
+ #=> true
69
+
70
+ ## expose method requires block
71
+ redacted_for_expose_check = RedactedString.new(@api_key)
72
+ begin
73
+ redacted_for_expose_check.expose
74
+ rescue ArgumentError => e
75
+ e.message
76
+ end
77
+ #=> "Block required"
78
+
79
+ ## expose method provides access to original value
80
+ redacted_for_expose = RedactedString.new("sk-1234567890abcdef")
81
+ result = nil
82
+ redacted_for_expose.expose { |val| result = val.dup }
83
+ result
84
+ #=> "sk-1234567890abcdef"
85
+
86
+ ## expose method does not automatically clear after use
87
+ redacted_single_use = RedactedString.new(@password)
88
+ redacted_single_use.expose { |val| val.length }
89
+ redacted_single_use.cleared?
90
+ #=> false
91
+
92
+ ## expose method does not clear if exception occurs
93
+ redacted_exception_test = RedactedString.new(@api_key)
94
+ begin
95
+ redacted_exception_test.expose { |val| raise "test error" }
96
+ rescue => e
97
+ # Exception occurred, but string should still be cleared
98
+ end
99
+ redacted_exception_test.cleared?
100
+ #=> false
101
+
102
+ ## expose method on cleared string raises SecurityError
103
+ cleared_redacted = RedactedString.new(@password)
104
+ cleared_redacted.clear!
105
+ begin
106
+ cleared_redacted.expose { |val| val }
107
+ rescue SecurityError => e
108
+ e.message
109
+ end
110
+ #=> "Value already cleared"
111
+
112
+ ## clear! method marks string as cleared
113
+ redacted_for_clear = RedactedString.new(@api_key)
114
+ redacted_for_clear.clear!
115
+ redacted_for_clear.cleared?
116
+ #=> true
117
+
118
+ ## clear! method is safe to call multiple times
119
+ redacted_multi_clear = RedactedString.new(@password)
120
+ redacted_multi_clear.clear!
121
+ redacted_multi_clear.clear! # Second call
122
+ redacted_multi_clear.cleared?
123
+ #=> true
124
+
125
+ ## clear! method freezes the object
126
+ redacted_freeze_test = RedactedString.new(@api_key)
127
+ redacted_freeze_test.clear!
128
+ redacted_freeze_test.frozen?
129
+ #=> true
130
+
131
+ ## Equality comparison only true for same object (prevents timing attacks)
132
+ redacted1 = RedactedString.new(@api_key)
133
+ redacted2 = RedactedString.new(@api_key)
134
+ redacted1 == redacted2
135
+ #=> false
136
+
137
+ ## Same object equality returns true
138
+ redacted_same = RedactedString.new(@password)
139
+ redacted_same == redacted_same
140
+ #=> true
141
+
142
+ ## eql? behaves same as ==
143
+ redacted_eql1 = RedactedString.new(@api_key)
144
+ redacted_eql2 = RedactedString.new(@api_key)
145
+ redacted_eql1.eql?(redacted_eql2)
146
+ #=> false
147
+
148
+ ## Same object eql? returns true
149
+ redacted_eql_same = RedactedString.new(@password)
150
+ redacted_eql_same.eql?(redacted_eql_same)
151
+ #=> true
152
+
153
+ ## All instances have same hash (prevents hash-based timing attacks)
154
+ redacted_hash1 = RedactedString.new(@api_key)
155
+ redacted_hash2 = RedactedString.new(@password)
156
+ redacted_hash1.hash == redacted_hash2.hash
157
+ #=> true
158
+
159
+ ## Hash value is consistent with class hash
160
+ redacted_hash_consistent = RedactedString.new(@api_key)
161
+ redacted_hash_consistent.hash == RedactedString.hash
162
+ #=> true
163
+
164
+ ## RedactedString cannot be used in string operations without expose
165
+ redacted_no_concat = RedactedString.new(@api_key)
166
+ begin
167
+ result = redacted_no_concat + "suffix"
168
+ false # Should not reach here
169
+ rescue => e
170
+ true # Expected to raise error
171
+ end
172
+ #=> true
173
+
174
+ ## RedactedString is not a String subclass (security by design)
175
+ redacted_type_check = RedactedString.new(@password)
176
+ redacted_type_check.is_a?(String)
177
+ #=> false
178
+
179
+ ## Working with empty strings
180
+ empty_redacted_test = RedactedString.new("")
181
+ result = nil
182
+ empty_redacted_test.expose { |val| result = val }
183
+ result
184
+ #=> ""
185
+
186
+ ## Working with long strings preserves content
187
+ long_redacted_test = RedactedString.new("a" * 100)
188
+ result = nil
189
+ long_redacted_test.expose { |val| result = val.length }
190
+ result
191
+ #=> 100
192
+
193
+ ## Special characters are preserved
194
+ special_redacted_test = RedactedString.new("päßwörd!@#$%^&*()")
195
+ result = nil
196
+ special_redacted_test.expose { |val| result = val.dup }
197
+ result
198
+ #=> "päßwörd!@#$%^&*()"
199
+
200
+ ## Finalizer proc exists and is callable
201
+ RedactedString.finalizer_proc.class
202
+ #=> Proc
203
+
204
+ ## Cleared redacted string maintains redacted appearance
205
+ cleared_appearance_test = RedactedString.new(@api_key)
206
+ cleared_appearance_test.clear!
207
+ cleared_appearance_test.to_s
208
+ #=> "[REDACTED]"
209
+
210
+ ## Cleared redacted string inspect still redacted
211
+ cleared_inspect_test = RedactedString.new(@password)
212
+ cleared_inspect_test.clear!
213
+ cleared_inspect_test.inspect
214
+ #=> "[REDACTED]"
215
+
216
+ ## Object created from nil input
217
+ nil_input_test = RedactedString.new(nil)
218
+ result = nil
219
+ nil_input_test.expose { |val| result = val.dup }
220
+ result
221
+ #=> ""
222
+
223
+ ## Numeric input converted to string
224
+ numeric_input_test = RedactedString.new(42)
225
+ result = nil
226
+ numeric_input_test.expose { |val| result = val.dup }
227
+ result
228
+ #=> "42"
229
+
230
+ ## Symbol input converted to string
231
+ symbol_input_test = RedactedString.new(:secret)
232
+ result = nil
233
+ symbol_input_test.expose { |val| result = val.dup }
234
+ result
235
+ #=> "secret"
236
+
237
+
238
+ # TEARDOWN
239
+
240
+ # Clean up any remaining test objects
241
+ @api_key = nil
242
+ @password = nil
243
+ @empty_secret = nil
244
+ @long_secret = nil
245
+ @special_chars = nil
246
+
247
+ # Force garbage collection to trigger any finalizers
248
+ GC.start
@@ -0,0 +1,164 @@
1
+ # try/features/transient_fields/refresh_reset_try.rb
2
+ # Test that refresh! properly resets transient fields to nil
3
+
4
+ require_relative '../../helpers/test_helpers'
5
+
6
+ Familia.debug = false
7
+
8
+ Familia.dbclient.flushdb
9
+
10
+ class SecretService < Familia::Horreum
11
+ identifier_field :name
12
+
13
+ field :name
14
+ field :endpoint_url
15
+
16
+ transient_field :api_key
17
+ transient_field :password
18
+ transient_field :secret_token, as: :token
19
+ end
20
+
21
+ @service = SecretService.new
22
+ @service.name = 'test-service'
23
+ @service.endpoint_url = 'https://api.example.com'
24
+ @service.api_key = 'sk-1234567890abcdef'
25
+ @service.password = 'super-secret-password'
26
+ @service.token = 'token-xyz789'
27
+
28
+
29
+ ## Verify class has the expected fields
30
+ SecretService.fields.sort
31
+ #=> [:api_key, :endpoint_url, :name, :password, :secret_token]
32
+
33
+ ## Verify service was created successfully
34
+ @service.nil?
35
+ #=> false
36
+
37
+ ## Save persistent fields to database
38
+ @service.save
39
+ #=> true
40
+
41
+ ## Verify transient fields have values before refresh
42
+ @service.api_key.nil?
43
+ #=> false
44
+
45
+ ## Verify transient fields are RedactedString instances
46
+ @service.api_key
47
+ #=:> RedactedString
48
+
49
+ ## Verify transient fields will not expose the value like a string
50
+ @service.api_key.to_s
51
+ #=> '[REDACTED]'
52
+
53
+ ## Verify transient fields will expose the value when asked
54
+ @service.api_key.value
55
+ #=> 'sk-1234567890abcdef'
56
+
57
+ ## Verify password field has value before refresh
58
+ @service.password.nil?
59
+ #=> false
60
+
61
+ ## Verify token alias has value before refresh
62
+ @service.token.nil?
63
+ #=> false
64
+
65
+ ## Verify persistent fields have values before refresh
66
+ @service.name
67
+ #=> "test-service"
68
+
69
+ ## Verify endpoint_url has value before refresh
70
+ @service.endpoint_url
71
+ #=> "https://api.example.com"
72
+
73
+ ## Refresh! should reset transient fields to nil but keep persistent ones
74
+ @service.refresh!
75
+ #=> [:name, :endpoint_url]
76
+
77
+ ## After refresh!, transient fields should be nil
78
+ @service.api_key.nil?
79
+ #=> true
80
+
81
+ ## After refresh!, password should be nil
82
+ @service.password.nil?
83
+ #=> true
84
+
85
+ ## After refresh!, token alias should be nil
86
+ @service.token.nil?
87
+ #=> true
88
+
89
+ ## After refresh!, persistent fields should retain their values
90
+ @service.name
91
+ #=> "test-service"
92
+
93
+ ## After refresh!, endpoint_url should retain its value
94
+ @service.endpoint_url
95
+ #=> "https://api.example.com"
96
+
97
+ ## Set transient fields again after refresh
98
+ @service.api_key = 'new-api-key-after-refresh'
99
+ @service.password = 'new-password-after-refresh'
100
+ @service.token = 'new-token-after-refresh'
101
+ #=> 'new-token-after-refresh'
102
+
103
+ ## Verify transient fields have new values
104
+ @service.api_key.nil?
105
+ #=> false
106
+
107
+ ## Verify they're still RedactedString instances
108
+ @service.api_key
109
+ #=:> RedactedString
110
+
111
+ ## Another refresh! should reset them again
112
+ @service.refresh!
113
+ #=> [:name, :endpoint_url]
114
+
115
+ ## Transient fields should be nil again
116
+ @service.api_key.nil?
117
+ #=> true
118
+
119
+ ## Password should be nil again
120
+ @service.password.nil?
121
+ #=> true
122
+
123
+ ## Token should be nil again
124
+ @service.token.nil?
125
+ #=> true
126
+
127
+ ## But persistent fields should remain intact
128
+ @service.name
129
+ #=> "test-service"
130
+
131
+ ## Endpoint URL should remain intact
132
+ @service.endpoint_url
133
+ #=> "https://api.example.com"
134
+
135
+ ## Test refresh! with object that has no transient fields
136
+ class SimpleService < Familia::Horreum
137
+ identifier_field :id
138
+ field :id
139
+ field :name
140
+ field :status
141
+ end
142
+
143
+ @no_transient = SimpleService.new('no-transient-test')
144
+ @no_transient.name = 'No Transient Service'
145
+ @no_transient.status = 'active'
146
+
147
+ # Save and refresh should work normally without transient fields
148
+ @no_transient.save
149
+ @no_transient.refresh!
150
+
151
+ # All fields should retain their values
152
+ @no_transient.id
153
+ #=> "no-transient-test"
154
+
155
+ ## Name should be preserved
156
+ @no_transient.name
157
+ #=> "No Transient Service"
158
+
159
+ ## Status should be preserved
160
+ @no_transient.status
161
+ #=> "active"
162
+
163
+
164
+ [@service, @no_transient].each(&:destroy!)
@@ -0,0 +1,50 @@
1
+
2
+ require_relative '../../helpers/test_helpers'
3
+
4
+ Familia.debug = false
5
+ Familia.dbclient.flushdb
6
+
7
+ # Use existing Customer class (should work)
8
+ service = Customer.new('refresh-test-customer')
9
+ puts "Created customer: #{service.class}"
10
+ puts "Customer identifier: #{service.identifier}"
11
+
12
+ # Add a transient field for testing
13
+ class Customer
14
+ transient_field :temp_data
15
+ end
16
+
17
+ # Set some values
18
+ service.name = 'Test Customer'
19
+ service.temp_data = 'secret-info'
20
+
21
+ puts "Before save:"
22
+ puts " name: #{service.name.inspect}"
23
+ puts " temp_data: #{service.temp_data.inspect}"
24
+ puts " temp_data class: #{service.temp_data.class}"
25
+
26
+ # Save to database
27
+ result = service.save
28
+ puts "Save result: #{result}"
29
+
30
+ puts "Before refresh:"
31
+ puts " name: #{service.name.inspect}"
32
+ puts " temp_data: #{service.temp_data.inspect}"
33
+ puts " temp_data nil?: #{service.temp_data.nil?}"
34
+
35
+ # Refresh should reset transient field to nil but keep persistent field
36
+ service.refresh!
37
+ puts "After refresh:"
38
+ puts " name: #{service.name.inspect}"
39
+ puts " temp_data: #{service.temp_data.inspect}"
40
+ puts " temp_data nil?: #{service.temp_data.nil?}"
41
+
42
+ # Verify that the refresh! reset worked as expected
43
+ if service.temp_data.nil? && service.name == 'Test Customer'
44
+ puts "SUCCESS: refresh! properly reset transient field while preserving persistent field"
45
+ else
46
+ puts "FAILED: refresh! did not work as expected"
47
+ end
48
+
49
+ service.destroy!
50
+ puts "Test completed"