familia 2.0.0.pre4 → 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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop_todo.yml +17 -17
- data/CLAUDE.md +3 -3
- data/Gemfile +5 -1
- data/Gemfile.lock +18 -3
- data/README.md +36 -157
- data/TEST_COVERAGE.md +40 -0
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +270 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
- data/docs/wiki/Home.md +49 -0
- data/docs/wiki/Implementation-Guide.md +183 -0
- data/docs/wiki/Security-Model.md +143 -0
- data/lib/familia/base.rb +18 -27
- data/lib/familia/connection.rb +6 -5
- data/lib/familia/{datatype → data_type}/commands.rb +2 -5
- data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
- data/lib/familia/{datatype → data_type}/types/hashkey.rb +2 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +10 -12
- data/lib/familia/encryption/manager.rb +102 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -0
- data/lib/familia/encryption/registry.rb +50 -0
- data/lib/familia/encryption.rb +178 -0
- data/lib/familia/encryption_request_cache.rb +68 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +153 -0
- data/lib/familia/features/encrypted_fields.rb +28 -0
- data/lib/familia/features/expiration.rb +107 -77
- data/lib/familia/features/quantization.rb +5 -9
- data/lib/familia/features/relatable_objects.rb +2 -4
- data/lib/familia/features/safe_dump.rb +14 -17
- data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
- data/lib/familia/features/transient_fields.rb +47 -0
- data/lib/familia/features.rb +40 -24
- data/lib/familia/field_type.rb +270 -0
- data/lib/familia/horreum/connection.rb +8 -11
- data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
- data/lib/familia/horreum/definition_methods.rb +453 -0
- data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -243
- data/lib/familia/horreum/serialization.rb +46 -18
- data/lib/familia/horreum/settings.rb +10 -2
- data/lib/familia/horreum/utils.rb +9 -10
- data/lib/familia/horreum.rb +18 -10
- data/lib/familia/logging.rb +14 -14
- data/lib/familia/settings.rb +39 -3
- data/lib/familia/utils.rb +45 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -1
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -1
- data/try/core/errors_try.rb +0 -1
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +0 -1
- data/try/core/pools_try.rb +2 -2
- data/try/core/secure_identifier_try.rb +0 -1
- data/try/core/settings_try.rb +0 -1
- data/try/core/utils_try.rb +0 -1
- data/try/{datatypes → data_types}/boolean_try.rb +1 -2
- data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
- data/try/{datatypes → data_types}/hash_try.rb +1 -2
- data/try/{datatypes → data_types}/list_try.rb +1 -2
- data/try/{datatypes → data_types}/set_try.rb +1 -2
- data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
- data/try/{datatypes → data_types}/string_try.rb +1 -2
- data/try/debugging/README.md +32 -0
- data/try/debugging/cache_behavior_tracer.rb +91 -0
- data/try/debugging/encryption_method_tracer.rb +138 -0
- data/try/debugging/provider_diagnostics.rb +110 -0
- data/try/edge_cases/hash_symbolization_try.rb +0 -1
- data/try/edge_cases/json_serialization_try.rb +0 -1
- data/try/edge_cases/reserved_keywords_try.rb +42 -11
- data/try/encryption/config_persistence_try.rb +192 -0
- data/try/encryption/encryption_core_try.rb +328 -0
- data/try/encryption/instance_variable_scope_try.rb +31 -0
- data/try/encryption/module_loading_try.rb +28 -0
- data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
- data/try/encryption/roundtrip_validation_try.rb +28 -0
- data/try/encryption/secure_memory_handling_try.rb +125 -0
- data/try/features/encrypted_fields_core_try.rb +117 -0
- data/try/features/encrypted_fields_integration_try.rb +220 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
- data/try/features/encrypted_fields_security_try.rb +370 -0
- data/try/features/encryption_fields/aad_protection_try.rb +53 -0
- data/try/features/encryption_fields/context_isolation_try.rb +120 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
- data/try/features/encryption_fields/fresh_key_try.rb +163 -0
- data/try/features/encryption_fields/key_rotation_try.rb +117 -0
- data/try/features/encryption_fields/memory_security_try.rb +37 -0
- data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +54 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -0
- data/try/features/expiration_try.rb +0 -1
- data/try/features/feature_dependencies_try.rb +159 -0
- data/try/features/quantization_try.rb +0 -1
- data/try/features/real_feature_integration_try.rb +148 -0
- data/try/features/relatable_objects_try.rb +0 -1
- data/try/features/safe_dump_advanced_try.rb +0 -1
- data/try/features/safe_dump_try.rb +0 -1
- data/try/features/transient_fields/redacted_string_try.rb +248 -0
- data/try/features/transient_fields/refresh_reset_try.rb +164 -0
- data/try/features/transient_fields/simple_refresh_test.rb +50 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
- data/try/features/transient_fields_core_try.rb +181 -0
- data/try/features/transient_fields_integration_try.rb +260 -0
- data/try/helpers/test_helpers.rb +42 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
- data/try/horreum/field_categories_try.rb +118 -0
- data/try/horreum/field_definition_try.rb +96 -0
- data/try/horreum/initialization_try.rb +0 -1
- data/try/horreum/relations_try.rb +0 -1
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +2 -3
- data/try/memory/memory_basic_test.rb +73 -0
- data/try/memory/memory_detailed_test.rb +121 -0
- data/try/memory/memory_docker_ruby_dump.sh +80 -0
- data/try/memory/memory_search_for_string.rb +83 -0
- data/try/memory/test_actual_redactedstring_protection.rb +38 -0
- data/try/models/customer_safe_dump_try.rb +0 -1
- data/try/models/customer_try.rb +0 -1
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- 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
|
@@ -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
|
@@ -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
|