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.
- checksums.yaml +4 -4
- data/CLAUDE.md +8 -5
- data/Gemfile +1 -1
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +600 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +72 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/RelatableObjects-Guide.md +563 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/expiration.rb +1 -1
- data/lib/familia/features/quantization.rb +1 -1
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +1 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +44 -28
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/encryption/encryption_core_try.rb +3 -3
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
- data/try/features/encryption_fields/context_isolation_try.rb +29 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +25 -0
- data/try/horreum/enhanced_conflict_handling_try.rb +1 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +1 -1
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +1 -1
- metadata +51 -10
- data/TEST_COVERAGE.md +0 -40
- 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
|
72
|
-
# Total: 10 threads *
|
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
|
-
#=>
|
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 #
|
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 ==
|
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.
|
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.
|
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.
|
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
|
-
#=> [
|
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
|
-
#=> [
|
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
|
data/try/helpers/test_helpers.rb
CHANGED
@@ -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[
|
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
|
-
#=>
|
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
|
-
#=> [
|
63
|
+
#=> ["description", "email", "id", "metadata", "name"]
|
64
64
|
|
65
65
|
## to_h includes all persistent fields
|
66
|
-
@hash_result.key?(
|
66
|
+
@hash_result.key?("name")
|
67
67
|
#=> true
|
68
68
|
|
69
69
|
## to_h includes encrypted persistent fields
|
70
|
-
@hash_result.key?(
|
70
|
+
@hash_result.key?("email")
|
71
71
|
#=> true
|
72
72
|
|
73
73
|
## to_h includes explicitly persistent fields
|
74
|
-
@hash_result.key?(
|
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[
|
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
|
-
#=> {
|
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
|
-
#=> [
|
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[
|
147
|
+
@aliased_test.to_h["internal_name"]
|
148
148
|
#=> "Updated Name"
|
149
149
|
|
150
150
|
## Clear fields respects field method map
|