familia 2.0.0.pre3 → 2.0.0.pre5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +3 -3
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +18 -3
  7. data/README.md +36 -157
  8. data/TEST_COVERAGE.md +40 -0
  9. data/docs/overview.md +359 -0
  10. data/docs/wiki/API-Reference.md +270 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
  12. data/docs/wiki/Home.md +49 -0
  13. data/docs/wiki/Implementation-Guide.md +183 -0
  14. data/docs/wiki/Security-Model.md +143 -0
  15. data/lib/familia/base.rb +18 -27
  16. data/lib/familia/connection.rb +6 -5
  17. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  18. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  19. data/lib/familia/{datatype → data_type}/types/hashkey.rb +2 -2
  20. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  21. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  22. data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
  23. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  24. data/lib/familia/{datatype.rb → data_type.rb} +10 -12
  25. data/lib/familia/encryption/manager.rb +102 -0
  26. data/lib/familia/encryption/provider.rb +49 -0
  27. data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
  28. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  29. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -0
  30. data/lib/familia/encryption/registry.rb +50 -0
  31. data/lib/familia/encryption.rb +178 -0
  32. data/lib/familia/encryption_request_cache.rb +68 -0
  33. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +153 -0
  34. data/lib/familia/features/encrypted_fields.rb +28 -0
  35. data/lib/familia/features/expiration.rb +107 -77
  36. data/lib/familia/features/quantization.rb +5 -9
  37. data/lib/familia/features/relatable_objects.rb +2 -4
  38. data/lib/familia/features/safe_dump.rb +14 -17
  39. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  40. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  41. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  42. data/lib/familia/features/transient_fields.rb +47 -0
  43. data/lib/familia/features.rb +40 -24
  44. data/lib/familia/field_type.rb +270 -0
  45. data/lib/familia/horreum/connection.rb +8 -11
  46. data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
  47. data/lib/familia/horreum/definition_methods.rb +453 -0
  48. data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
  49. data/lib/familia/horreum/serialization.rb +46 -18
  50. data/lib/familia/horreum/settings.rb +10 -2
  51. data/lib/familia/horreum/utils.rb +9 -10
  52. data/lib/familia/horreum.rb +18 -10
  53. data/lib/familia/logging.rb +14 -14
  54. data/lib/familia/settings.rb +39 -3
  55. data/lib/familia/utils.rb +45 -0
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -1
  58. data/try/core/base_enhancements_try.rb +115 -0
  59. data/try/core/connection_try.rb +0 -1
  60. data/try/core/errors_try.rb +0 -1
  61. data/try/core/familia_extended_try.rb +3 -4
  62. data/try/core/familia_try.rb +0 -1
  63. data/try/core/pools_try.rb +2 -2
  64. data/try/core/secure_identifier_try.rb +0 -1
  65. data/try/core/settings_try.rb +0 -1
  66. data/try/core/utils_try.rb +0 -1
  67. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  68. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  69. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  70. data/try/{datatypes → data_types}/list_try.rb +1 -2
  71. data/try/{datatypes → data_types}/set_try.rb +1 -2
  72. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  73. data/try/{datatypes → data_types}/string_try.rb +1 -2
  74. data/try/debugging/README.md +32 -0
  75. data/try/debugging/cache_behavior_tracer.rb +91 -0
  76. data/try/debugging/encryption_method_tracer.rb +138 -0
  77. data/try/debugging/provider_diagnostics.rb +110 -0
  78. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  79. data/try/edge_cases/json_serialization_try.rb +0 -1
  80. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  81. data/try/encryption/config_persistence_try.rb +192 -0
  82. data/try/encryption/encryption_core_try.rb +328 -0
  83. data/try/encryption/instance_variable_scope_try.rb +31 -0
  84. data/try/encryption/module_loading_try.rb +28 -0
  85. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  86. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  87. data/try/encryption/roundtrip_validation_try.rb +28 -0
  88. data/try/encryption/secure_memory_handling_try.rb +125 -0
  89. data/try/features/encrypted_fields_core_try.rb +117 -0
  90. data/try/features/encrypted_fields_integration_try.rb +220 -0
  91. data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
  92. data/try/features/encrypted_fields_security_try.rb +370 -0
  93. data/try/features/encryption_fields/aad_protection_try.rb +53 -0
  94. data/try/features/encryption_fields/context_isolation_try.rb +120 -0
  95. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  96. data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
  97. data/try/features/encryption_fields/fresh_key_try.rb +163 -0
  98. data/try/features/encryption_fields/key_rotation_try.rb +117 -0
  99. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  100. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  101. data/try/features/encryption_fields/nonce_uniqueness_try.rb +54 -0
  102. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  103. data/try/features/expiration_try.rb +0 -1
  104. data/try/features/feature_dependencies_try.rb +159 -0
  105. data/try/features/quantization_try.rb +0 -1
  106. data/try/features/real_feature_integration_try.rb +148 -0
  107. data/try/features/relatable_objects_try.rb +0 -1
  108. data/try/features/safe_dump_advanced_try.rb +0 -1
  109. data/try/features/safe_dump_try.rb +0 -1
  110. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  111. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  112. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  113. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  114. data/try/features/transient_fields_core_try.rb +181 -0
  115. data/try/features/transient_fields_integration_try.rb +260 -0
  116. data/try/helpers/test_helpers.rb +42 -0
  117. data/try/horreum/base_try.rb +157 -3
  118. data/try/horreum/class_methods_try.rb +27 -36
  119. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  120. data/try/horreum/field_categories_try.rb +118 -0
  121. data/try/horreum/field_definition_try.rb +96 -0
  122. data/try/horreum/initialization_try.rb +0 -1
  123. data/try/horreum/relations_try.rb +0 -1
  124. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  125. data/try/horreum/serialization_try.rb +2 -3
  126. data/try/memory/memory_basic_test.rb +73 -0
  127. data/try/memory/memory_detailed_test.rb +121 -0
  128. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  129. data/try/memory/memory_search_for_string.rb +83 -0
  130. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  131. data/try/models/customer_safe_dump_try.rb +0 -1
  132. data/try/models/customer_try.rb +0 -1
  133. data/try/models/datatype_base_try.rb +1 -2
  134. data/try/models/familia_object_try.rb +0 -1
  135. metadata +85 -18
@@ -0,0 +1,54 @@
1
+ # try/features/encryption_fields/nonce_uniqueness_try.rb
2
+
3
+ require 'base64'
4
+ require 'set'
5
+
6
+ require_relative '../../helpers/test_helpers'
7
+
8
+ test_keys = {
9
+ v1: Base64.strict_encode64('a' * 32),
10
+ }
11
+ Familia.config.encryption_keys = test_keys
12
+ Familia.config.current_key_version = :v1
13
+
14
+ class NonceTest < Familia::Horreum
15
+ feature :encrypted_fields
16
+ identifier_field :id
17
+ field :id
18
+ encrypted_field :secret
19
+ end
20
+
21
+ ## Multiple encryptions produce unique nonces
22
+ model = NonceTest.new(id: 'nonce-test')
23
+ nonces = Set.new
24
+
25
+ 10.times do
26
+ model.secret = 'same-value'
27
+ cipher = JSON.parse(model.instance_variable_get(:@secret))
28
+ nonces.add(cipher['nonce'])
29
+ end
30
+
31
+ nonces.size == 10
32
+ #=> true
33
+
34
+ ## Each encryption generates a unique nonce even for identical data
35
+ @model2 = NonceTest.new(id: 'nonce-test-2')
36
+
37
+ # Encrypt same value twice
38
+ @model2.secret = 'duplicate-test'
39
+ @cipher1 = JSON.parse(@model2.instance_variable_get(:@secret))
40
+
41
+ @model2.secret = 'duplicate-test'
42
+ @cipher2 = JSON.parse(@model2.instance_variable_get(:@secret))
43
+
44
+ # Nonces should be different
45
+ @cipher1['nonce'] != @cipher2['nonce']
46
+ #=> true
47
+
48
+ ## Ciphertexts are also different due to different nonces
49
+ @cipher1['ciphertext'] != @cipher2['ciphertext']
50
+ #=> true
51
+
52
+ # Cleanup
53
+ Familia.config.encryption_keys = nil
54
+ Familia.config.current_key_version = nil
@@ -0,0 +1,199 @@
1
+ # try/features/encryption_fields/thread_safety_try.rb
2
+
3
+ require 'concurrent'
4
+ require 'base64'
5
+
6
+ require_relative '../../helpers/test_helpers'
7
+
8
+ # Setup encryption keys for testing
9
+ test_keys = {
10
+ v1: Base64.strict_encode64('a' * 32),
11
+ v2: Base64.strict_encode64('b' * 32)
12
+ }
13
+ Familia.config.encryption_keys = test_keys
14
+ Familia.config.current_key_version = :v1
15
+
16
+ class ThreadTest < Familia::Horreum
17
+ feature :encrypted_fields
18
+ identifier_field :id
19
+ field :id
20
+ encrypted_field :secret
21
+ end
22
+
23
+ # Thread-safe debug logging helper
24
+ @debug_mutex = Mutex.new
25
+ def debug(msg)
26
+ return unless ENV['FAMILIA_DEBUG']
27
+ @debug_mutex.synchronize { puts "DEBUG: #{msg}" }
28
+ end
29
+
30
+ ## Concurrent encryption operations maintain counter integrity
31
+ Familia::Encryption.reset_derivation_count!
32
+ @results = Concurrent::Array.new
33
+ @errors = Concurrent::Array.new
34
+
35
+ debug "Starting 10 threads for concurrent operations..."
36
+
37
+ @threads = 10.times.map do |i|
38
+ Thread.new do
39
+ begin
40
+ model = ThreadTest.new(id: "thread-#{i}")
41
+ 5.times do |j|
42
+ model.secret = "secret-#{i}-#{j}" # encrypt (derivation)
43
+ retrieved = model.secret # decrypt (derivation)
44
+ @results << retrieved
45
+ end
46
+ debug "Thread #{i} completed successfully"
47
+ rescue => e
48
+ debug "Thread #{i} failed: #{e.class}: #{e.message}"
49
+ @errors << e
50
+ end
51
+ end
52
+ end
53
+
54
+ @threads.each(&:join)
55
+
56
+ debug "All threads joined. Results: #{@results.size}, Errors: #{@errors.size}"
57
+ debug "Derivation count: #{Familia::Encryption.derivation_count.value}"
58
+
59
+ if @errors.any?
60
+ debug "Error details:"
61
+ @errors.each_with_index { |e, i| debug " #{i+1}. #{e.class}: #{e.message}" }
62
+ end
63
+
64
+ @errors.empty?
65
+ #=> true
66
+
67
+ ## All expected results collected (10 threads × 5 operations = 50 results)
68
+ @results.size
69
+ #=> 50
70
+
71
+ ## Each thread did 5 write + 5 read = 10 derivations
72
+ # Total: 10 threads * 10 derivations = 100
73
+ Familia::Encryption.derivation_count.value
74
+ #=> 100
75
+
76
+ ## Key rotation operations work safely under concurrent access
77
+ debug "Starting key rotation test with 4 threads..."
78
+
79
+ @rotation_errors = Concurrent::Array.new
80
+ @rotation_results = Concurrent::Array.new
81
+
82
+ @rotation_threads = 4.times.map do |i|
83
+ Thread.new do
84
+ begin
85
+ # Each thread alternates between v1 and v2
86
+ thread_version = i.even? ? :v1 : :v2
87
+
88
+ 10.times do |j|
89
+ debug "Thread #{i}, iteration #{j}, using version #{thread_version}"
90
+
91
+ # Temporarily switch key version for this operation
92
+ original_version = Familia.config.current_key_version
93
+ Familia.config.current_key_version = thread_version
94
+
95
+ begin
96
+ model = ThreadTest.new(id: "race-#{i}-#{j}")
97
+ model.secret = "test-#{i}-#{j}" # encrypt
98
+ retrieved = model.secret # decrypt
99
+ @rotation_results << retrieved
100
+ debug "Thread #{i}, iteration #{j} completed"
101
+ ensure
102
+ # Restore original version
103
+ Familia.config.current_key_version = original_version
104
+ end
105
+ end
106
+ rescue => e
107
+ debug "Rotation thread #{i} failed: #{e.class}: #{e.message}"
108
+ @rotation_errors << e
109
+ end
110
+ end
111
+ end
112
+
113
+ @rotation_threads.each(&:join)
114
+
115
+ debug "Key rotation test completed. Results: #{@rotation_results.size}, Errors: #{@rotation_errors.size}"
116
+
117
+ if @rotation_errors.any?
118
+ debug "Rotation error details:"
119
+ @rotation_errors.each_with_index { |e, i| debug " #{i+1}. #{e.class}: #{e.message}" }
120
+ end
121
+
122
+ @rotation_errors.empty?
123
+ #=> true
124
+
125
+ ## All rotation operations completed successfully (4 threads × 10 operations = 40 results)
126
+ @rotation_results.size
127
+ #=> 40
128
+
129
+ ## Atomic counter maintains accuracy under maximum contention
130
+ debug "Starting atomic counter test with 20 threads..."
131
+ debug "Count before reset: #{Familia::Encryption.derivation_count.value}"
132
+
133
+ Familia::Encryption.reset_derivation_count!
134
+ sleep(0.01) # Minimal delay to ensure reset takes effect
135
+
136
+ debug "Count after reset: #{Familia::Encryption.derivation_count.value}"
137
+
138
+ barrier = Concurrent::CyclicBarrier.new(20)
139
+ @counter_errors = Concurrent::Array.new
140
+
141
+ counter_threads = 20.times.map do |i|
142
+ Thread.new do
143
+ begin
144
+ barrier.wait # Synchronize start for maximum contention
145
+ model = ThreadTest.new(id: "counter-test-#{i}")
146
+ model.secret = 'test' # Single encrypt operation (1 derivation)
147
+ debug "Counter thread #{i} completed"
148
+ rescue => e
149
+ debug "Counter thread #{i} failed: #{e.class}: #{e.message}"
150
+ @counter_errors << e
151
+ end
152
+ end
153
+ end
154
+
155
+ counter_threads.each(&:join)
156
+
157
+ debug "Atomic counter test completed. Final count: #{Familia::Encryption.derivation_count.value}"
158
+
159
+ @counter_errors.empty?
160
+ #=> true
161
+
162
+ ## Exactly 20 derivations, no lost increments
163
+ Familia::Encryption.derivation_count.value
164
+ #=> 20
165
+
166
+ ## Concurrent encryption operations maintain counter integrity
167
+ class ThreadTest2 < Familia::Horreum
168
+ feature :encrypted_fields
169
+ identifier_field :id
170
+ field :id
171
+ encrypted_field :secret
172
+ end
173
+
174
+ Familia::Encryption.reset_derivation_count!
175
+ errors = Concurrent::Array.new
176
+ barrier = Concurrent::CyclicBarrier.new(10)
177
+
178
+ threads = 10.times.map do |i|
179
+ Thread.new do
180
+ barrier.wait # Synchronize start
181
+ model = ThreadTest.new(id: "thread-#{i}")
182
+ begin
183
+ 5.times { |j|
184
+ model.secret = "value-#{i}-#{j}"
185
+ model.secret # decrypt
186
+ }
187
+ rescue => e
188
+ errors << e
189
+ end
190
+ end
191
+ end
192
+
193
+ threads.each(&:join)
194
+ errors.empty? && Familia::Encryption.derivation_count.value == 100
195
+ #=> true
196
+
197
+ # Cleanup
198
+ Familia.config.encryption_keys = nil
199
+ Familia.config.current_key_version = nil
@@ -1,6 +1,5 @@
1
1
  # try/features/expiration_try.rb
2
2
 
3
- require_relative '../../lib/familia'
4
3
  require_relative '../helpers/test_helpers'
5
4
 
6
5
  Familia.debug = false
@@ -0,0 +1,159 @@
1
+ # try/features/feature_dependencies_try.rb
2
+
3
+ require_relative '../helpers/test_helpers'
4
+
5
+ Familia.debug = false
6
+
7
+ # Create test features with dependencies for testing
8
+ module TestFeatureA
9
+ def self.included(base)
10
+ Familia.ld "[#{base}] Loaded #{self}"
11
+ base.extend ClassMethods
12
+ end
13
+
14
+ module ClassMethods
15
+ def test_feature_a_method
16
+ "feature_a_active"
17
+ end
18
+ end
19
+
20
+ def test_feature_a_instance
21
+ "instance_feature_a"
22
+ end
23
+ end
24
+
25
+ module TestFeatureB
26
+ def self.included(base)
27
+ Familia.ld "[#{base}] Loaded #{self}"
28
+ base.extend ClassMethods
29
+ end
30
+
31
+ module ClassMethods
32
+ def test_feature_b_method
33
+ "feature_b_active"
34
+ end
35
+ end
36
+
37
+ def test_feature_b_instance
38
+ "instance_feature_b"
39
+ end
40
+ end
41
+
42
+ module TestFeatureCWithDeps
43
+ def self.included(base)
44
+ Familia.ld "[#{base}] Loaded #{self}"
45
+ base.extend ClassMethods
46
+ end
47
+
48
+ module ClassMethods
49
+ def test_feature_c_method
50
+ "feature_c_with_deps_active"
51
+ end
52
+ end
53
+
54
+ def test_feature_c_instance
55
+ "instance_feature_c_with_deps"
56
+ end
57
+ end
58
+
59
+ # Register test features manually
60
+ Familia::Base.add_feature TestFeatureA, :test_feature_a
61
+ Familia::Base.add_feature TestFeatureB, :test_feature_b
62
+ Familia::Base.add_feature TestFeatureCWithDeps, :test_feature_c, depends_on: [:test_feature_a, :test_feature_b]
63
+
64
+ ## Feature definitions are created correctly
65
+ Familia::Base.feature_definitions.key?(:test_feature_a)
66
+ #=> true
67
+
68
+ ## Feature definitions store dependencies correctly
69
+ Familia::Base.feature_definitions[:test_feature_c].depends_on
70
+ #=> [:test_feature_a, :test_feature_b]
71
+
72
+ ## Features without dependencies have empty depends_on array
73
+ Familia::Base.feature_definitions[:test_feature_a].depends_on
74
+ #=> []
75
+
76
+ ## Feature definitions store name correctly
77
+ Familia::Base.feature_definitions[:test_feature_c].name
78
+ #=> :test_feature_c
79
+
80
+ ## Successfully enable feature without dependencies
81
+ class NoDepsTest < Familia::Horreum
82
+ identifier_field :id
83
+ field :id
84
+ feature :test_feature_a
85
+ end
86
+ @nodeps = NoDepsTest.new(id: 'test1')
87
+ @nodeps.test_feature_a_instance
88
+ #=> "instance_feature_a"
89
+
90
+ ## Successfully enable multiple features in correct order
91
+ class MultiFeatureTest < Familia::Horreum
92
+ identifier_field :id
93
+ field :id
94
+ feature :test_feature_a
95
+ feature :test_feature_b
96
+ feature :test_feature_c
97
+ end
98
+ @multitest = MultiFeatureTest.new(id: 'test2')
99
+ @multitest.test_feature_c_instance
100
+ #=> "instance_feature_c_with_deps"
101
+
102
+ ## Class methods from dependent features are available
103
+ MultiFeatureTest.test_feature_c_method
104
+ #=> "feature_c_with_deps_active"
105
+
106
+ ## All prerequisite features are available in features_enabled
107
+ MultiFeatureTest.features_enabled.include?(:test_feature_a)
108
+ #=> true
109
+
110
+ ## All prerequisite features are available
111
+ MultiFeatureTest.features_enabled.include?(:test_feature_b)
112
+ #=> true
113
+
114
+ ## Dependent feature is available
115
+ MultiFeatureTest.features_enabled.include?(:test_feature_c)
116
+ #=> true
117
+
118
+ ## Feature dependency validation fails when dependencies missing
119
+ class MissingDepsTest < Familia::Horreum
120
+ identifier_field :id
121
+ field :id
122
+ feature :test_feature_c # Missing dependencies should cause error
123
+ end
124
+ #=!> Familia::Problem
125
+
126
+ ## Partial dependencies cause validation failure
127
+ class PartialDepsTest < Familia::Horreum
128
+ identifier_field :id
129
+ field :id
130
+ feature :test_feature_a # Only one of two required dependencies
131
+ feature :test_feature_c
132
+ end
133
+ #=!> Familia::Problem
134
+
135
+ ## Invalid feature name raises appropriate error
136
+ class InvalidFeatureTest < Familia::Horreum
137
+ identifier_field :id
138
+ field :id
139
+ feature :nonexistent_feature
140
+ end
141
+ #=!> Familia::Problem
142
+
143
+ ## Duplicate feature inclusion gives warning but continues
144
+ class DuplicateFeatureTest < Familia::Horreum
145
+ identifier_field :id
146
+ field :id
147
+ feature :test_feature_a
148
+ feature :test_feature_a # Duplicate should warn
149
+ end
150
+ @duplicate_test = DuplicateFeatureTest.new(id: 'dup1')
151
+ @duplicate_test.test_feature_a_instance
152
+ #=> "instance_feature_a"
153
+
154
+ @nodeps.destroy! rescue nil
155
+ @multitest.destroy! rescue nil
156
+ @duplicate_test.destroy! rescue nil
157
+ @nodeps = nil
158
+ @multitest = nil
159
+ @duplicate_test = nil
@@ -1,6 +1,5 @@
1
1
  # try/features/quantization_try.rb
2
2
 
3
- require_relative '../../lib/familia'
4
3
  require_relative '../helpers/test_helpers'
5
4
 
6
5
  Familia.debug = false
@@ -0,0 +1,148 @@
1
+ # try/features/real_feature_integration_try.rb
2
+
3
+ require_relative '../helpers/test_helpers'
4
+
5
+ Familia.debug = false
6
+
7
+ # Real feature integration: expiration feature works with new system
8
+ class ExpirationIntegrationTest < Familia::Horreum
9
+ identifier_field :id
10
+ field :id
11
+ field :name
12
+ feature :expiration
13
+ end
14
+
15
+ # Safe dump feature integration with field categories
16
+ class SafeDumpCategoryTest < Familia::Horreum
17
+ identifier_field :id
18
+ field :id
19
+ field :public_name, category: :persistent
20
+ field :email, category: :encrypted
21
+ field :tryouts_cache_data, category: :transient
22
+
23
+ feature :safe_dump
24
+
25
+ @safe_dump_fields = [
26
+ :id,
27
+ :public_name,
28
+ :email
29
+ ]
30
+ end
31
+
32
+ # Combined features work together
33
+ class CombinedFeaturesTest < Familia::Horreum
34
+ identifier_field :id
35
+ field :id
36
+ field :name, category: :persistent
37
+ field :temp_data, category: :transient
38
+
39
+ feature :expiration
40
+ feature :safe_dump
41
+
42
+ @safe_dump_fields = [:id, :name]
43
+ end
44
+
45
+ # Test that individual features can be queried
46
+ class QueryFeaturesTest < Familia::Horreum
47
+ identifier_field :id
48
+ field :id
49
+ feature :expiration
50
+ end
51
+
52
+ # Empty features list for class without features
53
+ class NoFeaturesTest < Familia::Horreum
54
+ identifier_field :id
55
+ field :id
56
+ end
57
+
58
+ # Error handling for duplicate feature includes
59
+ class DuplicateFeatureHandling < Familia::Horreum
60
+ identifier_field :id
61
+ field :id
62
+ feature :expiration
63
+ # This should generate a warning but not error
64
+ feature :expiration
65
+ end
66
+
67
+ @expiration_test = ExpirationIntegrationTest.new(id: 'exp_test_1', name: 'Test')
68
+
69
+ @safedump_test = SafeDumpCategoryTest.new(
70
+ id: 'safe_test_1',
71
+ public_name: 'Public Name',
72
+ email: 'test@example.com',
73
+ tryouts_cache_data: 'temporary'
74
+ )
75
+
76
+ @combined_test = CombinedFeaturesTest.new(id: 'combined_1', name: 'Combined', temp_data: 'temp')
77
+
78
+ ## Expiration feature is properly registered
79
+ Familia::Base.features_available.key?(:expiration)
80
+ #=> true
81
+
82
+ ## Feature enabled correctly
83
+ ExpirationIntegrationTest.features_enabled.include?(:expiration)
84
+ #=> true
85
+
86
+ ## Expiration methods are available
87
+ @expiration_test.respond_to?(:update_expiration)
88
+ #=> true
89
+
90
+ ## Class methods from expiration feature work
91
+ ExpirationIntegrationTest.respond_to?(:default_expiration)
92
+ #=> true
93
+
94
+ ## Safe dump feature loaded correctly
95
+ SafeDumpCategoryTest.features_enabled.include?(:safe_dump)
96
+ #=> true
97
+
98
+ ## Safe dump works with field categories
99
+ @safedump_result = @safedump_test.safe_dump
100
+ @safedump_result.keys.sort
101
+ #=> [:email, :id, :public_name]
102
+
103
+ ## Safe dump respects safe_dump_fields configuration
104
+ @safedump_result.key?(:tryouts_cache_data)
105
+ #=> false
106
+
107
+ ## Both features are enabled
108
+ CombinedFeaturesTest.features_enabled.include?(:expiration)
109
+ #=> true
110
+
111
+ ## Safe dump feature also enabled
112
+ CombinedFeaturesTest.features_enabled.include?(:safe_dump)
113
+ #=> true
114
+
115
+ ## Combined functionality works correctly
116
+ @combined_test.safe_dump
117
+ #=> { id: "combined_1", name: "Combined" }
118
+
119
+ ## Expiration functionality still available
120
+ @combined_test.respond_to?(:update_expiration)
121
+ #=> true
122
+
123
+ ## Test that feature() method returns current features when called with no args
124
+ CombinedFeaturesTest.feature
125
+ #=> [:expiration, :safe_dump]
126
+
127
+ ## Test that features_enabled() method returns the same results as feature() method
128
+ CombinedFeaturesTest.feature
129
+ #=> [:expiration, :safe_dump]
130
+
131
+ ## Features list is accessible
132
+ QueryFeaturesTest.feature
133
+ #=> [:expiration]
134
+
135
+ ## No features returns empty array
136
+ NoFeaturesTest.feature
137
+ #=> []
138
+
139
+ ## Duplicate features handled gracefully
140
+ DuplicateFeatureHandling.features_enabled
141
+ #=> [:expiration]
142
+
143
+ @expiration_test.destroy! rescue nil
144
+ @safedump_test.destroy! rescue nil
145
+ @combined_test.destroy! rescue nil
146
+ @expiration_test = nil
147
+ @safedump_test = nil
148
+ @combined_test = nil
@@ -2,7 +2,6 @@
2
2
 
3
3
  # Test RelatableObject feature functionality
4
4
 
5
- require_relative '../../lib/familia'
6
5
  require_relative '../helpers/test_helpers'
7
6
 
8
7
  Familia.debug = false
@@ -2,7 +2,6 @@
2
2
 
3
3
  # These tryouts test the safe dumping functionality.
4
4
 
5
- require_relative '../../lib/familia'
6
5
  require_relative '../helpers/test_helpers'
7
6
 
8
7
  ## By default Familia::Base has no safe_dump_fields method
@@ -1,6 +1,5 @@
1
1
  # try/features/safe_dump_try.rb
2
2
 
3
- require_relative '../../lib/familia'
4
3
  require_relative '../helpers/test_helpers'
5
4
 
6
5
  Familia.debug = false