familia 2.0.0.pre5 → 2.0.0.pre6

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +8 -5
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +4 -3
  5. data/docs/wiki/API-Reference.md +95 -18
  6. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  7. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  8. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  9. data/docs/wiki/Feature-System-Guide.md +600 -0
  10. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  11. data/docs/wiki/Field-System-Guide.md +784 -0
  12. data/docs/wiki/Home.md +72 -15
  13. data/docs/wiki/Implementation-Guide.md +126 -33
  14. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  15. data/docs/wiki/RelatableObjects-Guide.md +563 -0
  16. data/docs/wiki/Security-Model.md +65 -25
  17. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  18. data/lib/familia/base.rb +1 -1
  19. data/lib/familia/data_type/types/counter.rb +38 -0
  20. data/lib/familia/data_type/types/hashkey.rb +18 -0
  21. data/lib/familia/data_type/types/lock.rb +43 -0
  22. data/lib/familia/data_type/types/string.rb +9 -2
  23. data/lib/familia/data_type.rb +2 -2
  24. data/lib/familia/encryption/encrypted_data.rb +137 -0
  25. data/lib/familia/encryption/manager.rb +21 -4
  26. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  27. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  28. data/lib/familia/encryption.rb +1 -1
  29. data/lib/familia/errors.rb +17 -3
  30. data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
  31. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  32. data/lib/familia/features/expiration.rb +1 -1
  33. data/lib/familia/features/quantization.rb +1 -1
  34. data/lib/familia/features/safe_dump.rb +1 -1
  35. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  36. data/lib/familia/features/transient_fields.rb +1 -1
  37. data/lib/familia/field_type.rb +5 -2
  38. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  39. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  40. data/lib/familia/horreum/core/serialization.rb +535 -0
  41. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  42. data/lib/familia/horreum/core.rb +21 -0
  43. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  44. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +44 -28
  45. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  46. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  47. data/lib/familia/horreum.rb +17 -17
  48. data/lib/familia/version.rb +1 -1
  49. data/lib/familia.rb +1 -1
  50. data/try/core/create_method_try.rb +240 -0
  51. data/try/core/database_consistency_try.rb +299 -0
  52. data/try/core/errors_try.rb +25 -4
  53. data/try/core/familia_try.rb +1 -1
  54. data/try/core/persistence_operations_try.rb +297 -0
  55. data/try/data_types/counter_try.rb +93 -0
  56. data/try/data_types/lock_try.rb +133 -0
  57. data/try/debugging/debug_aad_process.rb +82 -0
  58. data/try/debugging/debug_concealed_internal.rb +59 -0
  59. data/try/debugging/debug_concealed_reveal.rb +61 -0
  60. data/try/debugging/debug_context_aad.rb +68 -0
  61. data/try/debugging/debug_context_simple.rb +80 -0
  62. data/try/debugging/debug_cross_context.rb +62 -0
  63. data/try/debugging/debug_database_load.rb +64 -0
  64. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  65. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  66. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  67. data/try/debugging/debug_field_decrypt.rb +74 -0
  68. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  69. data/try/debugging/debug_load_path.rb +66 -0
  70. data/try/debugging/debug_method_definition.rb +46 -0
  71. data/try/debugging/debug_method_resolution.rb +41 -0
  72. data/try/debugging/debug_minimal.rb +24 -0
  73. data/try/debugging/debug_provider.rb +68 -0
  74. data/try/debugging/debug_secure_behavior.rb +73 -0
  75. data/try/debugging/debug_string_class.rb +46 -0
  76. data/try/debugging/debug_test.rb +46 -0
  77. data/try/debugging/debug_test_design.rb +80 -0
  78. data/try/encryption/encryption_core_try.rb +3 -3
  79. data/try/features/encrypted_fields_core_try.rb +19 -11
  80. data/try/features/encrypted_fields_integration_try.rb +66 -70
  81. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  82. data/try/features/encrypted_fields_security_try.rb +151 -144
  83. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  84. data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
  85. data/try/features/encryption_fields/context_isolation_try.rb +29 -8
  86. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  87. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  88. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  89. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  90. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  91. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  92. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  93. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  94. data/try/features/feature_dependencies_try.rb +3 -3
  95. data/try/features/transient_fields_core_try.rb +1 -1
  96. data/try/features/transient_fields_integration_try.rb +1 -1
  97. data/try/helpers/test_helpers.rb +25 -0
  98. data/try/horreum/enhanced_conflict_handling_try.rb +1 -1
  99. data/try/horreum/initialization_try.rb +1 -1
  100. data/try/horreum/relations_try.rb +1 -1
  101. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  102. data/try/horreum/serialization_try.rb +39 -4
  103. data/try/models/customer_safe_dump_try.rb +1 -1
  104. data/try/models/customer_try.rb +1 -1
  105. metadata +51 -10
  106. data/TEST_COVERAGE.md +0 -40
  107. data/lib/familia/horreum/serialization.rb +0 -473
@@ -0,0 +1,310 @@
1
+ # try/features/encryption_fields/secure_by_default_behavior_try.rb
2
+
3
+ require_relative '../../helpers/test_helpers'
4
+ require 'base64'
5
+
6
+ Familia.debug = false
7
+
8
+ # Configure encryption keys
9
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
10
+ Familia.config.encryption_keys = test_keys
11
+ Familia.config.current_key_version = :v1
12
+
13
+ # Test class demonstrating secure patterns
14
+ class SecureUserAccount < Familia::Horreum
15
+ feature :encrypted_fields
16
+ identifier_field :id
17
+ field :id
18
+ field :username # Public field
19
+ field :email # Public field
20
+ encrypted_field :password_hash # Secure field
21
+ encrypted_field :api_secret # Secure field
22
+ encrypted_field :recovery_key # Secure field
23
+ end
24
+
25
+ # Clean test environment
26
+ Familia.dbclient.flushdb
27
+
28
+ # Create test user
29
+ @user = SecureUserAccount.new
30
+ @user.id = "user123"
31
+ @user.username = "john_doe"
32
+ @user.email = "john@example.com"
33
+ @user.password_hash = "bcrypt$2a$12$abcdef..."
34
+ @user.api_secret = "sk-1234567890abcdef"
35
+ @user.recovery_key = "recovery-key-xyz789"
36
+
37
+ ## Public field returns String class and value
38
+ @user.username
39
+ #=:> String
40
+ #=> "john_doe"
41
+
42
+ ## Email field returns correct value
43
+ @user.email
44
+ #=> "john@example.com"
45
+
46
+ ## Password hash returns ConcealedString - no auto-decryption
47
+ @user.password_hash.class.name
48
+ #=> "ConcealedString"
49
+
50
+ ## API secret returns ConcealedString
51
+ @user.api_secret.class.name
52
+ #=> "ConcealedString"
53
+
54
+ ## Recovery key returns ConcealedString
55
+ @user.recovery_key.class.name
56
+ #=> "ConcealedString"
57
+
58
+ ## Explicit reveal required for password access
59
+ revealed_password = nil
60
+ @user.password_hash.reveal do |plaintext|
61
+ revealed_password = plaintext
62
+ end
63
+ revealed_password
64
+ #=> "bcrypt$2a$12$abcdef..."
65
+
66
+ ## Multiple encrypted fields require individual reveals
67
+ revealed_secret = nil
68
+ @user.api_secret.reveal do |plaintext|
69
+ revealed_secret = plaintext
70
+ end
71
+ revealed_secret
72
+ #=> "sk-1234567890abcdef"
73
+
74
+ ## String operations on encrypted fields are safe
75
+ password_string = @user.password_hash.to_s
76
+ password_string.include?("bcrypt")
77
+ #=> false
78
+
79
+ ## to_s returns concealed marker
80
+ @user.password_hash.to_s
81
+ #=> "[CONCEALED]"
82
+
83
+ ## inspect is safe for debugging
84
+ inspect_result = @user.password_hash.inspect
85
+ inspect_result.include?("bcrypt")
86
+ #=> false
87
+
88
+ ## inspect returns concealed marker
89
+ @user.password_hash.inspect
90
+ #=> "[CONCEALED]"
91
+
92
+ ## Array operations are secure
93
+ all_fields = [@user.username, @user.password_hash, @user.api_secret]
94
+ field_strings = all_fields.map(&:to_s)
95
+ field_strings
96
+ #=> ["john_doe", "[CONCEALED]", "[CONCEALED]"]
97
+
98
+ ## Hash serialization excludes encrypted fields
99
+ user_hash = @user.to_h
100
+ user_hash.keys.include?("password_hash")
101
+ #=> false
102
+
103
+ ## API secret also excluded from serialization
104
+ @user.to_h.keys.include?("api_secret")
105
+ #=> false
106
+
107
+ ## Recovery key excluded from serialization
108
+ @user.to_h.keys.include?("recovery_key")
109
+ #=> false
110
+
111
+ ## Only public fields included in serialization
112
+ @user.to_h.keys.sort
113
+ #=> ["email", "id", "username"]
114
+
115
+ ## Database operations preserve security
116
+ @user.save
117
+ #=> true
118
+
119
+ ## Fresh load from database returns ConcealedString
120
+ @fresh_user = SecureUserAccount.load("user123")
121
+ @fresh_user.password_hash.class.name
122
+ #=> "ConcealedString"
123
+
124
+ ## Fresh user API secret is concealed
125
+ @fresh_user.api_secret.class.name
126
+ #=> "ConcealedString"
127
+
128
+ ## Plaintext still requires explicit reveal after reload
129
+ revealed_fresh = nil
130
+ @fresh_user.password_hash.reveal do |plaintext|
131
+ revealed_fresh = plaintext
132
+ end
133
+ revealed_fresh
134
+ #=> "bcrypt$2a$12$abcdef..."
135
+
136
+ ## Regular field string operations work normally
137
+ @user.username.upcase
138
+ #=> "JOHN_DOE"
139
+
140
+ ## Regular field length access
141
+ @user.username.length
142
+ #=> 8
143
+
144
+ ## Regular field substring access
145
+ @user.username[0..3]
146
+ #=> "john"
147
+
148
+ ## Encrypted field string operations are protected
149
+ @user.password_hash.upcase
150
+ #=> "[CONCEALED]"
151
+
152
+ ## Encrypted field length is concealed length
153
+ @user.password_hash.length
154
+ #=> 11
155
+
156
+ ## Encrypted field substring is from concealed text
157
+ @user.password_hash.to_s[0..3]
158
+ #=> "[CON"
159
+
160
+ ## Logging patterns are safe
161
+ @log_message = "User #{@user.username} with password #{@user.password_hash}"
162
+ @log_message.include?("bcrypt")
163
+ #=> false
164
+
165
+ ## Log message shows concealed value
166
+ @log_message
167
+ #=> "User john_doe with password [CONCEALED]"
168
+
169
+ ## Exception messages are safe
170
+ begin
171
+ raise StandardError, "Authentication failed for #{@user.password_hash}"
172
+ rescue StandardError => e
173
+ e.message.include?("bcrypt")
174
+ end
175
+ #=> false
176
+
177
+ ## String method chaining is safe
178
+ transformed = @user.password_hash.downcase.strip.gsub(/secret/, "public")
179
+ transformed
180
+ #=> "[CONCEALED]"
181
+
182
+ ## Concatenation fails safely without to_str method (prevents accidental implicit conversion)
183
+ begin
184
+ @combined = "Password: " + @user.password_hash + " (encrypted)"
185
+ "concatenation_should_fail"
186
+ rescue TypeError => e
187
+ e.message.include?("no implicit conversion")
188
+ end
189
+ #=> true
190
+
191
+ ## JSON serialization is safe
192
+ @user_json = {
193
+ id: @user.id,
194
+ username: @user.username,
195
+ password: @user.password_hash
196
+ }.to_json
197
+
198
+ @user_json.include?("bcrypt")
199
+ #=> false
200
+
201
+ ## JSON contains concealed marker
202
+ @user_json.include?("[CONCEALED]")
203
+ #=> true
204
+
205
+ ## Bulk field operations are secure
206
+ @encrypted_fields = [:password_hash, :api_secret, :recovery_key]
207
+ @field_values = @encrypted_fields.map { |field| @user.send(field) }
208
+
209
+ @field_values.all? { |val| val.class.name == "ConcealedString" }
210
+ #=> true
211
+
212
+ ## All serialize safely
213
+ @safe_strings = @field_values.map(&:to_s)
214
+ @safe_strings.all? { |str| str == "[CONCEALED]" }
215
+ #=> true
216
+
217
+ ## Conditional operations are safe
218
+ password_present = @user.password_hash.present?
219
+ password_present
220
+ #=> true
221
+
222
+ ## Empty check is safe
223
+ password_empty = @user.password_hash.empty?
224
+ password_empty
225
+ #=> false
226
+
227
+ ## Nil check works
228
+ (@user.password_hash.nil?)
229
+ #=> false
230
+
231
+ ## Assignment maintains security
232
+ @user.api_secret = "new-secret-key-456"
233
+ @user.api_secret.class.name
234
+ #=> "ConcealedString"
235
+
236
+ ## New assignment serializes safely
237
+ @user.api_secret.to_s
238
+ #=> "[CONCEALED]"
239
+
240
+ ## Updating with reveal access to old value
241
+ old_value = nil
242
+ new_value = "updated-secret-789"
243
+
244
+ @user.api_secret.reveal do |current|
245
+ old_value = current
246
+ @user.api_secret = new_value
247
+ end
248
+
249
+ old_value
250
+ #=> "new-secret-key-456"
251
+
252
+ ## Updated value accessible via reveal
253
+ @user.api_secret.reveal { |x| x }
254
+ #=> "updated-secret-789"
255
+
256
+ ## Nil assignment returns nil
257
+ @user.recovery_key = nil
258
+ @user.recovery_key
259
+ #=> nil
260
+
261
+ ## Nil encrypted fields are NilClass
262
+ @user.recovery_key.class.name
263
+ #=> "NilClass"
264
+
265
+ ## Setting back to value returns to ConcealedString
266
+ @user.recovery_key = "new-recovery-key"
267
+ @user.recovery_key.class.name
268
+ #=> "ConcealedString"
269
+
270
+ ## Common mistake patterns are secure - string interpolation
271
+ search_pattern = "password_hash:#{@user.password_hash}"
272
+ search_pattern.include?("bcrypt")
273
+ #=> false
274
+
275
+ ## API response safety
276
+ api_response = {
277
+ user_id: @user.id,
278
+ credentials: {
279
+ password: @user.password_hash,
280
+ api_key: @user.api_secret
281
+ }
282
+ }
283
+
284
+ @response_json = api_response.to_json
285
+ @response_json.include?("bcrypt")
286
+ #=> false
287
+
288
+ ## API response doesn't leak secrets
289
+ @response_json.include?("sk-1234567890abcdef")
290
+ #=> false
291
+
292
+ ## API response contains concealed markers
293
+ @response_json.include?("[CONCEALED]")
294
+ #=> true
295
+
296
+ ## Debug logging safety
297
+ @debug_values = @user.instance_variables.map do |var|
298
+ "#{var}=#{@user.instance_variable_get(var)}"
299
+ end
300
+
301
+ @debug_string = @debug_values.join(", ")
302
+ @debug_string.include?("bcrypt")
303
+ #=> false
304
+
305
+ ## Debug output contains concealed markers
306
+ @debug_string.include?("[CONCEALED]")
307
+ #=> true
308
+
309
+ # Teardown
310
+ Familia.dbclient.flushdb
@@ -68,10 +68,10 @@ end
68
68
  @results.size
69
69
  #=> 50
70
70
 
71
- ## Each thread did 5 write + 5 read = 10 derivations
72
- # Total: 10 threads * 10 derivations = 100
71
+ ## Each thread did 5 write operations = 5 derivations
72
+ # Total: 10 threads * 5 derivations = 50 (reading returns ConcealedString without decryption)
73
73
  Familia::Encryption.derivation_count.value
74
- #=> 100
74
+ #=> 50
75
75
 
76
76
  ## Key rotation operations work safely under concurrent access
77
77
  debug "Starting key rotation test with 4 threads..."
@@ -181,8 +181,8 @@ threads = 10.times.map do |i|
181
181
  model = ThreadTest.new(id: "thread-#{i}")
182
182
  begin
183
183
  5.times { |j|
184
- model.secret = "value-#{i}-#{j}"
185
- model.secret # decrypt
184
+ model.secret = "value-#{i}-#{j}" # encrypt (derivation)
185
+ model.secret # returns ConcealedString (no derivation)
186
186
  }
187
187
  rescue => e
188
188
  errors << e
@@ -191,7 +191,7 @@ threads = 10.times.map do |i|
191
191
  end
192
192
 
193
193
  threads.each(&:join)
194
- errors.empty? && Familia::Encryption.derivation_count.value == 100
194
+ errors.empty? && Familia::Encryption.derivation_count.value == 50
195
195
  #=> true
196
196
 
197
197
  # Cleanup
@@ -0,0 +1,174 @@
1
+ # try/features/encryption_fields/universal_serialization_safety_try.rb
2
+
3
+ require_relative '../../helpers/test_helpers'
4
+ require 'base64'
5
+
6
+ Familia.debug = false
7
+
8
+ # Configure encryption keys
9
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
10
+ Familia.config.encryption_keys = test_keys
11
+ Familia.config.current_key_version = :v1
12
+
13
+ # Test class with mixed field types
14
+ class DataRecord < Familia::Horreum
15
+ feature :encrypted_fields
16
+ identifier_field :id
17
+ field :id
18
+ field :title # Public field
19
+ field :description # Public field
20
+ encrypted_field :api_token # Encrypted field
21
+ encrypted_field :secret_notes # Encrypted field
22
+ encrypted_field :user_data # Encrypted field
23
+ end
24
+
25
+ # Clean environment
26
+ Familia.dbclient.flushdb
27
+
28
+ # Create test record with mixed data
29
+ @record = DataRecord.new
30
+ @record.id = "rec001"
31
+ @record.title = "Public Record"
32
+ @record.description = "This is public information"
33
+ @record.api_token = "token-abc123456789"
34
+ @record.secret_notes = "confidential information"
35
+ @record.user_data = "sensitive user details"
36
+
37
+ ## Object-level to_h excludes encrypted fields
38
+ hash_result = @record.to_h
39
+ hash_result.keys.include?("api_token")
40
+ #=> false
41
+
42
+ ## to_h excludes secret notes
43
+ @record.to_h.keys.include?("secret_notes")
44
+ #=> false
45
+
46
+ ## to_h excludes user data
47
+ @record.to_h.keys.include?("user_data")
48
+ #=> false
49
+
50
+ ## to_h only includes public fields
51
+ @record.to_h.keys.sort
52
+ #=> ["description", "id", "title"]
53
+
54
+ ## to_h contains correct public values
55
+ @record.to_h["title"]
56
+ #=> "Public Record"
57
+
58
+ ## Individual encrypted field serialization safety - to_s
59
+ @record.api_token.to_s
60
+ #=> "[CONCEALED]"
61
+
62
+ ## to_str serialization
63
+ @record.api_token.to_str
64
+ #=!> NoMethodError
65
+
66
+ ## inspect serialization
67
+ @record.api_token.inspect
68
+ #=> "[CONCEALED]"
69
+
70
+ ## JSON serialization - to_json
71
+ @record.api_token.to_json
72
+ #=> "\"[CONCEALED]\""
73
+
74
+ ## JSON serialization - as_json
75
+ @record.api_token.as_json
76
+ #=> "[CONCEALED]"
77
+
78
+ ## Hash serialization
79
+ @record.api_token.to_h
80
+ #=> "[CONCEALED]"
81
+
82
+ ## Array serialization
83
+ @record.api_token.to_a
84
+ #=> ["[CONCEALED]"]
85
+
86
+ ## Numeric serialization - to_i
87
+ @record.api_token.to_i
88
+ #=!> NoMethodError
89
+
90
+ ## Float serialization
91
+ @record.api_token.to_f
92
+ #=!> NoMethodError
93
+
94
+ ## Complex nested JSON structure
95
+ @nested_data = {
96
+ record: @record,
97
+ fields: {
98
+ public: [@record.title, @record.description],
99
+ encrypted: [@record.api_token, @record.secret_notes]
100
+ }
101
+ }
102
+
103
+ @serialized = @nested_data.to_json
104
+ @serialized.include?("token-abc123456789")
105
+ #=> false
106
+
107
+ ## Nested JSON contains concealed markers
108
+ @nested_data.to_json.include?("[CONCEALED]")
109
+ #=> true
110
+
111
+ ## Array of mixed field types safety
112
+ @mixed_array = [
113
+ @record.title,
114
+ @record.api_token,
115
+ @record.description,
116
+ @record.secret_notes
117
+ ]
118
+
119
+ @mixed_array.to_json.include?("token-abc123456789")
120
+ #=> false
121
+
122
+ ## Mixed array preserves public data
123
+ @mixed_array.to_json.include?("Public Record")
124
+ #=> true
125
+
126
+ ## String interpolation safety
127
+ @debug_message = "Record #{@record.id}: token=#{@record.api_token}"
128
+ @debug_message.include?("token-abc123456789")
129
+ #=> false
130
+
131
+ ## Interpolation shows concealed values
132
+ @debug_message.include?("[CONCEALED]")
133
+ #=> true
134
+
135
+ ## Hash merge operations safety
136
+ merged_hash = @record.to_h.merge({
137
+ runtime_token: @record.api_token
138
+ })
139
+
140
+ merged_hash.values.any? { |v| v.to_s.include?("token-abc123456789") }
141
+ #=> false
142
+
143
+ ## Database persistence maintains safety
144
+ @record.save
145
+ #=> true
146
+
147
+ ## Fresh load serialization safety
148
+ @fresh_record = DataRecord.load("rec001")
149
+ @fresh_record.to_h.keys.include?("api_token")
150
+ #=> false
151
+
152
+ ## Fresh record field safety
153
+ @fresh_record.api_token.to_s
154
+ #=> "[CONCEALED]"
155
+
156
+ ## Exception handling safety
157
+ begin
158
+ raise StandardError, "Auth failed: #{@record.api_token}"
159
+ rescue StandardError => e
160
+ e.message.include?("token-abc123456789")
161
+ end
162
+ #=> false
163
+
164
+ ## String formatting safety
165
+ @formatted = "Token: %s" % [@record.api_token]
166
+ @formatted.include?("token-abc123456789")
167
+ #=> false
168
+
169
+ ## Formatted string shows concealed
170
+ @formatted.include?("[CONCEALED]")
171
+ #=> true
172
+
173
+ # Teardown
174
+ Familia.dbclient.flushdb
@@ -7,7 +7,7 @@ Familia.debug = false
7
7
  # Create test features with dependencies for testing
8
8
  module TestFeatureA
9
9
  def self.included(base)
10
- Familia.ld "[#{base}] Loaded #{self}"
10
+ Familia.trace :included, base, self, caller(1..1) if Familia.debug?
11
11
  base.extend ClassMethods
12
12
  end
13
13
 
@@ -24,7 +24,7 @@ end
24
24
 
25
25
  module TestFeatureB
26
26
  def self.included(base)
27
- Familia.ld "[#{base}] Loaded #{self}"
27
+ Familia.trace :INCLUDED, base, self, caller(1..1) if Familia.debug?
28
28
  base.extend ClassMethods
29
29
  end
30
30
 
@@ -41,7 +41,7 @@ end
41
41
 
42
42
  module TestFeatureCWithDeps
43
43
  def self.included(base)
44
- Familia.ld "[#{base}] Loaded #{self}"
44
+ Familia.trace :feature_load, base, self, caller(1..1) if Familia.debug?
45
45
  base.extend ClassMethods
46
46
  end
47
47
 
@@ -92,7 +92,7 @@ already_redacted = RedactedString.new('already_wrapped')
92
92
  ## Serialization to_h only includes persistent fields
93
93
  hash_result = @service.to_h
94
94
  hash_result.keys.sort
95
- #=> [:endpoint_url, :name]
95
+ #=> ["endpoint_url", "name"]
96
96
 
97
97
  ## Serialization to_h excludes api_key transient field
98
98
  hash_result = @service.to_h
@@ -102,7 +102,7 @@ already_redacted = RedactedString.new("already_wrapped")
102
102
  ## Serialization to_h only includes persistent fields
103
103
  hash_result = @service.to_h
104
104
  hash_result.keys.sort
105
- #=> [:endpoint_url, :name, :service_id]
105
+ #=> ["endpoint_url", "name", "service_id"]
106
106
 
107
107
  ## Serialization to_h excludes api_key transient field
108
108
  hash_result = @service.to_h
@@ -20,6 +20,8 @@ class Bone < Familia::Horreum
20
20
  zset :metrics
21
21
  hashkey :props
22
22
  string :value, default: 'GREAT!'
23
+ counter :counter, default: 0
24
+ lock :lock
23
25
  end
24
26
 
25
27
  class Blone < Familia::Horreum
@@ -204,3 +206,26 @@ module SingleUseRedactedStringTestHelper
204
206
  end
205
207
  end
206
208
  end
209
+
210
+ # ConcealedString test helper for accessing encrypted values in tests
211
+ unless defined?(ConcealedString)
212
+ require_relative '../../lib/familia/features/encrypted_fields/concealed_string'
213
+ end
214
+ module ConcealedStringTestHelper
215
+ refine ConcealedString do
216
+ # TEST-ONLY: Direct access to decrypted value
217
+ #
218
+ # This method bypasses the reveal block pattern and directly returns
219
+ # the decrypted plaintext. It should ONLY be used in test environments
220
+ # through refinements to keep this dangerous method out of production.
221
+ #
222
+ # @return [String] The decrypted plaintext value
223
+ #
224
+ def reveal_for_testing
225
+ raise SecurityError, 'Encrypted data already cleared' if cleared?
226
+ raise SecurityError, 'No encrypted data to reveal' if @encrypted_data.nil?
227
+
228
+ @field_type.decrypt_value(@record, @encrypted_data)
229
+ end
230
+ end
231
+ end
@@ -10,7 +10,7 @@ Familia::VALID_STRATEGIES.include?(:raise)
10
10
 
11
11
  ## Valid strategies include all expected options
12
12
  Familia::VALID_STRATEGIES
13
- #=> [:raise, :skip, :warn, :overwrite]
13
+ #=> [:raise, :skip, :ignore, :warn, :overwrite]
14
14
 
15
15
  ## Overwrite strategy removes existing method and defines new one
16
16
  class OverwriteStrategyTest < Familia::Horreum
@@ -49,7 +49,7 @@ Familia.debug = false
49
49
  #=> ["mixed@test.com", "Mixed Test", nil, "user"]
50
50
 
51
51
  ## to_h works correctly with keyword-initialized objects
52
- @customer2.to_h[:name]
52
+ @customer2.to_h["name"]
53
53
  #=> "Jane Smith"
54
54
 
55
55
  ## to_a works correctly with keyword-initialized objects
@@ -106,7 +106,7 @@ prefs = @test_user.preferences
106
106
  @test_product.views.increment
107
107
  @test_product.views.incrementby(5)
108
108
  @test_product.views.value
109
- #=> "6"
109
+ #=> 6
110
110
 
111
111
  ## Database types maintain parent reference
112
112
  @test_user.sessions.parent == @test_user
@@ -60,18 +60,18 @@ end
60
60
  ## to_h excludes transient fields
61
61
  @hash_result = @serialization_test.to_h
62
62
  @hash_result.keys.sort
63
- #=> [:description, :email, :id, :metadata, :name]
63
+ #=> ["description", "email", "id", "metadata", "name"]
64
64
 
65
65
  ## to_h includes all persistent fields
66
- @hash_result.key?(:name)
66
+ @hash_result.key?("name")
67
67
  #=> true
68
68
 
69
69
  ## to_h includes encrypted persistent fields
70
- @hash_result.key?(:email)
70
+ @hash_result.key?("email")
71
71
  #=> true
72
72
 
73
73
  ## to_h includes explicitly persistent fields
74
- @hash_result.key?(:description)
74
+ @hash_result.key?("description")
75
75
  #=> true
76
76
 
77
77
  ## to_h excludes transient fields from serialization
@@ -83,7 +83,7 @@ end
83
83
  #=> false
84
84
 
85
85
  ## to_h serializes complex values correctly
86
- @hash_result[:metadata]
86
+ @hash_result["metadata"]
87
87
  #=:> String
88
88
 
89
89
  ## to_a excludes transient fields
@@ -127,7 +127,7 @@ SerializationCategoryTest.persistent_fields.include?(:email)
127
127
 
128
128
  ## to_h with only id field when all others are transient
129
129
  @all_transient.to_h
130
- #=> { id: "transient_test_1" }
130
+ #=> {"id" => "transient_test_1"}
131
131
 
132
132
  ## to_a with only id field when all others are transient
133
133
  @all_transient.to_a
@@ -136,7 +136,7 @@ SerializationCategoryTest.persistent_fields.include?(:email)
136
136
  ## Aliased fields serialization uses original field names
137
137
  @aliased_hash = @aliased_test.to_h
138
138
  @aliased_hash.keys.sort
139
- #=> [:id, :internal_name, :user_data]
139
+ #=> ["id", "internal_name", "user_data"]
140
140
 
141
141
  ## Aliased transient fields are excluded
142
142
  @aliased_hash.key?(:temp_cache)
@@ -144,7 +144,7 @@ SerializationCategoryTest.persistent_fields.include?(:email)
144
144
 
145
145
  ## Serialization works with accessor methods through aliases
146
146
  @aliased_test.display_name = 'Updated Name'
147
- @aliased_test.to_h[:internal_name]
147
+ @aliased_test.to_h["internal_name"]
148
148
  #=> "Updated Name"
149
149
 
150
150
  ## Clear fields respects field method map