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.
Files changed (151) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -10
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +4 -3
  9. data/docs/wiki/API-Reference.md +95 -18
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +631 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +82 -15
  17. data/docs/wiki/Implementation-Guide.md +126 -33
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/Relationships-Guide.md +684 -0
  20. data/docs/wiki/Security-Model.md +65 -25
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/examples/bit_encoding_integration.rb +237 -0
  23. data/examples/redis_command_validation_example.rb +231 -0
  24. data/examples/relationships_basic.rb +273 -0
  25. data/lib/familia/base.rb +1 -1
  26. data/lib/familia/connection.rb +3 -3
  27. data/lib/familia/data_type/types/counter.rb +38 -0
  28. data/lib/familia/data_type/types/hashkey.rb +18 -0
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/data_type/types/string.rb +9 -2
  31. data/lib/familia/data_type.rb +9 -6
  32. data/lib/familia/encryption/encrypted_data.rb +137 -0
  33. data/lib/familia/encryption/manager.rb +21 -4
  34. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  35. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  36. data/lib/familia/encryption.rb +1 -1
  37. data/lib/familia/errors.rb +17 -3
  38. data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
  39. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  40. data/lib/familia/features/encrypted_fields.rb +413 -4
  41. data/lib/familia/features/expiration.rb +319 -33
  42. data/lib/familia/features/quantization.rb +385 -44
  43. data/lib/familia/features/relationships/cascading.rb +438 -0
  44. data/lib/familia/features/relationships/indexing.rb +370 -0
  45. data/lib/familia/features/relationships/membership.rb +503 -0
  46. data/lib/familia/features/relationships/permission_management.rb +264 -0
  47. data/lib/familia/features/relationships/querying.rb +620 -0
  48. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  49. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  50. data/lib/familia/features/relationships/tracking.rb +379 -0
  51. data/lib/familia/features/relationships.rb +466 -0
  52. data/lib/familia/features/safe_dump.rb +1 -1
  53. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  54. data/lib/familia/features/transient_fields.rb +192 -10
  55. data/lib/familia/features.rb +2 -1
  56. data/lib/familia/field_type.rb +5 -2
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  58. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  63. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
  64. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +17 -17
  67. data/lib/familia/validation/command_recorder.rb +336 -0
  68. data/lib/familia/validation/expectations.rb +519 -0
  69. data/lib/familia/validation/test_helpers.rb +443 -0
  70. data/lib/familia/validation/validator.rb +412 -0
  71. data/lib/familia/validation.rb +140 -0
  72. data/lib/familia/version.rb +1 -1
  73. data/lib/familia.rb +1 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -4
  77. data/try/core/familia_try.rb +1 -1
  78. data/try/core/persistence_operations_try.rb +297 -0
  79. data/try/data_types/counter_try.rb +93 -0
  80. data/try/data_types/lock_try.rb +133 -0
  81. data/try/debugging/debug_aad_process.rb +82 -0
  82. data/try/debugging/debug_concealed_internal.rb +59 -0
  83. data/try/debugging/debug_concealed_reveal.rb +61 -0
  84. data/try/debugging/debug_context_aad.rb +68 -0
  85. data/try/debugging/debug_context_simple.rb +80 -0
  86. data/try/debugging/debug_cross_context.rb +62 -0
  87. data/try/debugging/debug_database_load.rb +64 -0
  88. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  89. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  90. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  91. data/try/debugging/debug_field_decrypt.rb +74 -0
  92. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  93. data/try/debugging/debug_load_path.rb +66 -0
  94. data/try/debugging/debug_method_definition.rb +46 -0
  95. data/try/debugging/debug_method_resolution.rb +41 -0
  96. data/try/debugging/debug_minimal.rb +24 -0
  97. data/try/debugging/debug_provider.rb +68 -0
  98. data/try/debugging/debug_secure_behavior.rb +73 -0
  99. data/try/debugging/debug_string_class.rb +46 -0
  100. data/try/debugging/debug_test.rb +46 -0
  101. data/try/debugging/debug_test_design.rb +80 -0
  102. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  103. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  104. data/try/edge_cases/string_coercion_try.rb +2 -0
  105. data/try/encryption/encryption_core_try.rb +6 -4
  106. data/try/features/categorical_permissions_try.rb +515 -0
  107. data/try/features/encrypted_fields_core_try.rb +19 -11
  108. data/try/features/encrypted_fields_integration_try.rb +66 -70
  109. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  110. data/try/features/encrypted_fields_security_try.rb +151 -144
  111. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  112. data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
  113. data/try/features/encryption_fields/context_isolation_try.rb +30 -8
  114. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  115. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  116. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  117. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  118. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  119. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  120. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  121. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  122. data/try/features/feature_dependencies_try.rb +3 -3
  123. data/try/features/relationships_edge_cases_try.rb +145 -0
  124. data/try/features/relationships_performance_minimal_try.rb +132 -0
  125. data/try/features/relationships_performance_simple_try.rb +155 -0
  126. data/try/features/relationships_performance_try.rb +420 -0
  127. data/try/features/relationships_performance_working_try.rb +144 -0
  128. data/try/features/relationships_try.rb +237 -0
  129. data/try/features/safe_dump_try.rb +3 -0
  130. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  131. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  132. data/try/features/transient_fields_core_try.rb +1 -1
  133. data/try/features/transient_fields_integration_try.rb +1 -1
  134. data/try/helpers/test_helpers.rb +26 -1
  135. data/try/horreum/base_try.rb +14 -8
  136. data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
  137. data/try/horreum/initialization_try.rb +1 -1
  138. data/try/horreum/relations_try.rb +2 -2
  139. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  140. data/try/horreum/serialization_try.rb +39 -4
  141. data/try/models/customer_safe_dump_try.rb +1 -1
  142. data/try/models/customer_try.rb +1 -1
  143. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  144. data/try/validation/command_validation_try.rb.disabled +207 -0
  145. data/try/validation/performance_validation_try.rb.disabled +324 -0
  146. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  147. metadata +81 -12
  148. data/TEST_COVERAGE.md +0 -40
  149. data/lib/familia/features/relatable_objects.rb +0 -125
  150. data/lib/familia/horreum/serialization.rb +0 -473
  151. 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 + 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
 
@@ -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