familia 2.0.0.pre5 → 2.0.0.pre7
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/.github/workflows/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -10
- data/Gemfile +2 -2
- 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 +631 -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 +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- 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 +9 -6
- 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 +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- 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 +192 -10
- data/lib/familia/features.rb +2 -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} +45 -29
- 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/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- 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/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- 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 +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -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/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- 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 +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- 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
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- data/try/features/relatable_objects_try.rb +0 -220
@@ -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
|
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# Simplified edge case testing for Relationships v2 - focusing on core functionality
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
# Test classes for edge case testing
|
6
|
+
class EdgeTestCustomer < Familia::Horreum
|
7
|
+
feature :relationships
|
8
|
+
|
9
|
+
identifier_field :custid
|
10
|
+
field :custid
|
11
|
+
field :name
|
12
|
+
|
13
|
+
sorted_set :domains
|
14
|
+
set :simple_domains
|
15
|
+
list :domain_list
|
16
|
+
end
|
17
|
+
|
18
|
+
class EdgeTestDomain < Familia::Horreum
|
19
|
+
feature :relationships
|
20
|
+
|
21
|
+
identifier_field :domain_id
|
22
|
+
field :domain_id
|
23
|
+
field :display_domain
|
24
|
+
field :created_at
|
25
|
+
field :score_value
|
26
|
+
|
27
|
+
# Test different score calculation methods - simplified
|
28
|
+
tracked_in EdgeTestCustomer, :domains, score: :created_at, on_destroy: :remove
|
29
|
+
|
30
|
+
# Test different collection types for membership
|
31
|
+
member_of EdgeTestCustomer, :domains, type: :sorted_set
|
32
|
+
member_of EdgeTestCustomer, :simple_domains, type: :set
|
33
|
+
member_of EdgeTestCustomer, :domain_list, type: :list
|
34
|
+
|
35
|
+
def calculated_score
|
36
|
+
(score_value || 0) * 2
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Setup test data
|
41
|
+
@customer1 = EdgeTestCustomer.new(custid: 'cust1', name: 'Customer 1')
|
42
|
+
@customer2 = EdgeTestCustomer.new(custid: 'cust2', name: 'Customer 2')
|
43
|
+
|
44
|
+
@domain1 = EdgeTestDomain.new(
|
45
|
+
domain_id: 'edge_dom_1',
|
46
|
+
display_domain: 'edge1.example.com',
|
47
|
+
created_at: Time.new(2025, 6, 15, 12, 0, 0),
|
48
|
+
score_value: 10
|
49
|
+
)
|
50
|
+
|
51
|
+
@domain2 = EdgeTestDomain.new(
|
52
|
+
domain_id: 'edge_dom_2',
|
53
|
+
display_domain: 'edge2.example.com',
|
54
|
+
created_at: Time.new(2025, 7, 20, 15, 30, 0),
|
55
|
+
score_value: 25
|
56
|
+
)
|
57
|
+
|
58
|
+
# Score encoding edge cases
|
59
|
+
|
60
|
+
## Score encoding handles maximum metadata value
|
61
|
+
max_score = @domain1.encode_score(Time.now, 255)
|
62
|
+
decoded = @domain1.decode_score(max_score)
|
63
|
+
decoded[:permissions]
|
64
|
+
#=> 255
|
65
|
+
|
66
|
+
## Score encoding handles zero metadata
|
67
|
+
zero_score = @domain1.encode_score(Time.now, 0)
|
68
|
+
decoded_zero = @domain1.decode_score(zero_score)
|
69
|
+
decoded_zero[:permissions]
|
70
|
+
#=> 0
|
71
|
+
|
72
|
+
## Permission encoding handles unknown permission levels
|
73
|
+
unknown_perm_score = @domain1.permission_encode(Time.now, :unknown_permission)
|
74
|
+
decoded_unknown = @domain1.permission_decode(unknown_perm_score)
|
75
|
+
decoded_unknown[:permission_list]
|
76
|
+
#=> []
|
77
|
+
|
78
|
+
## Score encoding preserves precision for small timestamps
|
79
|
+
small_time = Time.at(1000000)
|
80
|
+
small_score = @domain1.encode_score(small_time, 50)
|
81
|
+
decoded_small = @domain1.decode_score(small_score)
|
82
|
+
(decoded_small[:timestamp] - small_time.to_f).abs < 0.01
|
83
|
+
#=> true
|
84
|
+
|
85
|
+
## Large timestamps encode correctly
|
86
|
+
large_time = Time.at(9999999999)
|
87
|
+
large_score = @domain1.encode_score(large_time, 123)
|
88
|
+
decoded_large = @domain1.decode_score(large_score)
|
89
|
+
decoded_large[:permissions]
|
90
|
+
#=> 123
|
91
|
+
|
92
|
+
## Permission encoding maps correctly
|
93
|
+
read_score = @domain1.permission_encode(Time.now, :read)
|
94
|
+
decoded_read = @domain1.permission_decode(read_score)
|
95
|
+
decoded_read[:permission_list].include?(:read)
|
96
|
+
#=> true
|
97
|
+
|
98
|
+
## Score encoding handles edge case timestamps
|
99
|
+
epoch_score = @domain1.encode_score(Time.at(0), 42)
|
100
|
+
decoded_epoch = @domain1.decode_score(epoch_score)
|
101
|
+
decoded_epoch[:permissions]
|
102
|
+
#=> 42
|
103
|
+
|
104
|
+
## Boundary score values work correctly
|
105
|
+
boundary_score = @domain1.encode_score(Time.now, 255)
|
106
|
+
decoded_boundary = @domain1.decode_score(boundary_score)
|
107
|
+
decoded_boundary[:permissions] <= 255
|
108
|
+
#=> true
|
109
|
+
|
110
|
+
# Basic functionality tests
|
111
|
+
|
112
|
+
## Method score calculation works with saved objects
|
113
|
+
@customer1.save
|
114
|
+
@domain1.save
|
115
|
+
@domain1.add_to_edgetestcustomer_domains(@customer1)
|
116
|
+
method_score = @domain1.score_in_edgetestcustomer_domains(@customer1)
|
117
|
+
method_score.is_a?(Float) && method_score > 0
|
118
|
+
#=> true
|
119
|
+
|
120
|
+
## Sorted set membership works
|
121
|
+
@domain1.in_edgetestcustomer_domains?(@customer1)
|
122
|
+
#=> true
|
123
|
+
|
124
|
+
## Score methods respond correctly
|
125
|
+
@domain1.respond_to?(:score_in_edgetestcustomer_domains)
|
126
|
+
#=> true
|
127
|
+
|
128
|
+
## Basic relationship cleanup works
|
129
|
+
@domain1.remove_from_edgetestcustomer_domains(@customer1)
|
130
|
+
@domain1.in_edgetestcustomer_domains?(@customer1)
|
131
|
+
#=> false
|
132
|
+
|
133
|
+
# Clean up test data
|
134
|
+
|
135
|
+
## Cleanup completes without errors
|
136
|
+
begin
|
137
|
+
[@customer1, @customer2, @domain1, @domain2].each do |obj|
|
138
|
+
obj.destroy if obj.respond_to?(:destroy)
|
139
|
+
end
|
140
|
+
true
|
141
|
+
rescue => e
|
142
|
+
puts "Cleanup error: #{e.message}"
|
143
|
+
false
|
144
|
+
end
|
145
|
+
#=> true
|