familia 2.0.0.pre4 → 2.0.0.pre6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop_todo.yml +17 -17
- data/CLAUDE.md +11 -8
- data/Gemfile +5 -1
- data/Gemfile.lock +19 -3
- data/README.md +36 -157
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +347 -0
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +600 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +106 -0
- data/docs/wiki/Implementation-Guide.md +276 -0
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/RelatableObjects-Guide.md +563 -0
- data/docs/wiki/Security-Model.md +183 -0
- data/docs/wiki/Transient-Fields-Guide.md +280 -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/data_type/types/counter.rb +38 -0
- data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +12 -14
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +119 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -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/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -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 +273 -0
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
- data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
- data/lib/familia/horreum/subclass/definition.rb +469 -0
- data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +30 -22
- 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 +3 -2
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -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 -5
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +1 -2
- data/try/core/persistence_operations_try.rb +297 -0
- 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/data_types/counter_try.rb +93 -0
- 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/data_types/lock_try.rb +133 -0
- 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/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/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 +125 -0
- data/try/features/encrypted_fields_integration_try.rb +216 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
- data/try/features/encrypted_fields_security_try.rb +377 -0
- data/try/features/encryption_fields/aad_protection_try.rb +138 -0
- data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
- data/try/features/encryption_fields/context_isolation_try.rb +141 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
- data/try/features/encryption_fields/fresh_key_try.rb +168 -0
- data/try/features/encryption_fields/key_rotation_try.rb +123 -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 +56 -0
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -0
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -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 +67 -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 +1 -2
- data/try/horreum/relations_try.rb +1 -2
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +41 -7
- 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 +1 -2
- data/try/models/customer_try.rb +1 -2
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +131 -23
- data/lib/familia/horreum/serialization.rb +0 -445
@@ -0,0 +1,260 @@
|
|
1
|
+
# try/features/transient_fields_integration_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
class SecretService < Familia::Horreum
|
6
|
+
feature :transient_fields
|
7
|
+
identifier_field :service_id
|
8
|
+
|
9
|
+
field :service_id
|
10
|
+
field :name
|
11
|
+
field :endpoint_url
|
12
|
+
transient_field :api_key
|
13
|
+
transient_field :password
|
14
|
+
transient_field :secret_token, as: :token
|
15
|
+
end
|
16
|
+
|
17
|
+
@service = SecretService.new
|
18
|
+
@service.service_id = 'test_service_1'
|
19
|
+
@service.name = 'Test API Service'
|
20
|
+
@service.endpoint_url = 'https://api.example.com'
|
21
|
+
@service.api_key = 'sk-1234567890abcdef'
|
22
|
+
@service.password = 'super_secret_password'
|
23
|
+
@service.token = 'token-xyz789'
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
## Class includes transient_fields feature
|
28
|
+
SecretService.feature(:transient_fields)
|
29
|
+
defined?(SecretService.feature)
|
30
|
+
#=> "method"
|
31
|
+
|
32
|
+
## Class has correct field definitions
|
33
|
+
SecretService.fields.sort
|
34
|
+
#=> [:api_key, :endpoint_url, :name, :password, :secret_token, :service_id]
|
35
|
+
|
36
|
+
## Persistent fields exclude transient ones
|
37
|
+
SecretService.persistent_fields.sort
|
38
|
+
#=> [:endpoint_url, :name, :service_id]
|
39
|
+
|
40
|
+
## Transient field definitions have correct category
|
41
|
+
SecretService.field_types[:api_key].category
|
42
|
+
#=> :transient
|
43
|
+
|
44
|
+
## Password field definition has correct category
|
45
|
+
SecretService.field_types[:password].category
|
46
|
+
#=> :transient
|
47
|
+
|
48
|
+
## Secret token field definition has correct category
|
49
|
+
SecretService.field_types[:secret_token].category
|
50
|
+
#=> :transient
|
51
|
+
|
52
|
+
## Regular field definition has correct category
|
53
|
+
SecretService.field_types[:name].category
|
54
|
+
#=> :field
|
55
|
+
|
56
|
+
## Transient field stores RedactedString object for api_key
|
57
|
+
@service.api_key.class
|
58
|
+
#=> RedactedString
|
59
|
+
|
60
|
+
## Transient field stores RedactedString object for password
|
61
|
+
@service.password.class
|
62
|
+
#=> RedactedString
|
63
|
+
|
64
|
+
## Transient field stores RedactedString object for token alias
|
65
|
+
@service.token.class
|
66
|
+
#=> RedactedString
|
67
|
+
|
68
|
+
## Regular field stores normal string value for name
|
69
|
+
@service.name.class
|
70
|
+
#=> String
|
71
|
+
|
72
|
+
## Regular field stores normal string value for endpoint_url
|
73
|
+
@service.endpoint_url.class
|
74
|
+
#=> String
|
75
|
+
|
76
|
+
## Transient field value is redacted in string representation
|
77
|
+
@service.api_key.to_s
|
78
|
+
#=> "[REDACTED]"
|
79
|
+
|
80
|
+
## Transient field value is redacted in inspect output
|
81
|
+
@service.password.inspect
|
82
|
+
#=> "[REDACTED]"
|
83
|
+
|
84
|
+
## Transient field can expose value securely through block
|
85
|
+
result = nil
|
86
|
+
@service.api_key.expose { |val| result = val.dup }
|
87
|
+
result
|
88
|
+
#=> "sk-1234567890abcdef"
|
89
|
+
|
90
|
+
## Transient field with custom method name exposes value correctly
|
91
|
+
result = nil
|
92
|
+
@service.token.expose { |val| result = val.dup }
|
93
|
+
result
|
94
|
+
#=> "token-xyz789"
|
95
|
+
|
96
|
+
## Setting transient field with existing RedactedString works
|
97
|
+
already_redacted = RedactedString.new("already_wrapped")
|
98
|
+
@service.password = already_redacted
|
99
|
+
@service.password.class
|
100
|
+
#=> RedactedString
|
101
|
+
|
102
|
+
## Serialization to_h only includes persistent fields
|
103
|
+
hash_result = @service.to_h
|
104
|
+
hash_result.keys.sort
|
105
|
+
#=> ["endpoint_url", "name", "service_id"]
|
106
|
+
|
107
|
+
## Serialization to_h excludes api_key transient field
|
108
|
+
hash_result = @service.to_h
|
109
|
+
hash_result.key?("api_key")
|
110
|
+
#=> false
|
111
|
+
|
112
|
+
## Serialization to_h excludes password transient field
|
113
|
+
hash_result = @service.to_h
|
114
|
+
hash_result.key?("password")
|
115
|
+
#=> false
|
116
|
+
|
117
|
+
## Serialization to_h excludes secret_token transient field
|
118
|
+
hash_result = @service.to_h
|
119
|
+
hash_result.key?("secret_token")
|
120
|
+
#=> false
|
121
|
+
|
122
|
+
## Serialization to_a only includes persistent field values
|
123
|
+
array_result = @service.to_a
|
124
|
+
array_result.length
|
125
|
+
#=> 3
|
126
|
+
|
127
|
+
## Array contains values in persistent field order
|
128
|
+
array_result = @service.to_a
|
129
|
+
persistent_fields_values = SecretService.persistent_fields.map { |f| @service.send(SecretService.field_types[f].method_name) }
|
130
|
+
array_result == persistent_fields_values
|
131
|
+
#=> true
|
132
|
+
|
133
|
+
## Database persistence only stores persistent fields
|
134
|
+
@service.save
|
135
|
+
raw_data = @service.hgetall
|
136
|
+
raw_data.keys.sort
|
137
|
+
#=> ["endpoint_url", "name", "service_id"]
|
138
|
+
|
139
|
+
## Raw Database data excludes api_key transient field
|
140
|
+
raw_data = @service.hgetall
|
141
|
+
raw_data.key?("api_key")
|
142
|
+
#=> false
|
143
|
+
|
144
|
+
## Raw Database data excludes password transient field
|
145
|
+
raw_data = @service.hgetall
|
146
|
+
raw_data.key?("password")
|
147
|
+
#=> false
|
148
|
+
|
149
|
+
## Raw Database data excludes secret_token transient field
|
150
|
+
raw_data = @service.hgetall
|
151
|
+
raw_data.key?("secret_token")
|
152
|
+
#=> false
|
153
|
+
|
154
|
+
## Database refresh only loads persistent fields name
|
155
|
+
fresh_service = SecretService.new
|
156
|
+
fresh_service.service_id = 'test_service_1'
|
157
|
+
fresh_service.refresh!
|
158
|
+
fresh_service.name
|
159
|
+
#=> "Test API Service"
|
160
|
+
|
161
|
+
## Database refresh only loads persistent fields endpoint_url
|
162
|
+
fresh_service = SecretService.new
|
163
|
+
fresh_service.service_id = 'test_service_1'
|
164
|
+
fresh_service.refresh!
|
165
|
+
fresh_service.endpoint_url
|
166
|
+
#=> "https://api.example.com"
|
167
|
+
|
168
|
+
## Database refresh leaves transient api_key as nil
|
169
|
+
fresh_service = SecretService.new
|
170
|
+
fresh_service.service_id = 'test_service_1'
|
171
|
+
fresh_service.refresh!
|
172
|
+
fresh_service.api_key
|
173
|
+
#=> nil
|
174
|
+
|
175
|
+
## Database refresh leaves transient password as nil
|
176
|
+
fresh_service = SecretService.new
|
177
|
+
fresh_service.service_id = 'test_service_1'
|
178
|
+
fresh_service.refresh!
|
179
|
+
fresh_service.password
|
180
|
+
#=> nil
|
181
|
+
|
182
|
+
## Database refresh leaves transient token as nil
|
183
|
+
fresh_service = SecretService.new
|
184
|
+
fresh_service.service_id = 'test_service_1'
|
185
|
+
fresh_service.refresh!
|
186
|
+
fresh_service.token
|
187
|
+
#=> nil
|
188
|
+
|
189
|
+
## String interpolation with transient field shows redacted value
|
190
|
+
log_message = "Connecting to #{@service.name} with key: #{@service.api_key}"
|
191
|
+
log_message.include?("[REDACTED]")
|
192
|
+
#=> true
|
193
|
+
|
194
|
+
## String interpolation with transient field hides actual value
|
195
|
+
log_message = "Connecting to #{@service.name} with key: #{@service.api_key}"
|
196
|
+
log_message.include?("sk-1234567890abcdef")
|
197
|
+
#=> false
|
198
|
+
|
199
|
+
## Hash containing transient field shows redacted in string output
|
200
|
+
config_hash = {
|
201
|
+
service: @service.name,
|
202
|
+
key: @service.api_key,
|
203
|
+
url: @service.endpoint_url
|
204
|
+
}
|
205
|
+
config_hash.to_s.include?("[REDACTED]")
|
206
|
+
#=> true
|
207
|
+
|
208
|
+
## Hash containing transient field hides actual value in string output
|
209
|
+
config_hash = {
|
210
|
+
service: @service.name,
|
211
|
+
key: @service.api_key,
|
212
|
+
url: @service.endpoint_url
|
213
|
+
}
|
214
|
+
config_hash.to_s.include?("sk-1234567890abcdef")
|
215
|
+
#=> false
|
216
|
+
|
217
|
+
## Exception messages with transient fields are safe
|
218
|
+
begin
|
219
|
+
raise StandardError, "Failed to authenticate with key: #{@service.api_key}"
|
220
|
+
rescue => e
|
221
|
+
e.message.include?("[REDACTED]")
|
222
|
+
end
|
223
|
+
#=> true
|
224
|
+
|
225
|
+
## Multiple transient field assignment creates RedactedString instances
|
226
|
+
new_service = SecretService.new
|
227
|
+
new_service.service_id = 'test_service_2'
|
228
|
+
new_service.name = 'Another Service'
|
229
|
+
new_service.api_key = 'new-api-key-123'
|
230
|
+
new_service.password = 'new-password-456'
|
231
|
+
new_service.token = 'new-token-789'
|
232
|
+
[new_service.api_key, new_service.password, new_service.token].all? { |f| f.is_a?(RedactedString) }
|
233
|
+
#=> true
|
234
|
+
|
235
|
+
## Transient field can be set to nil value
|
236
|
+
new_service = SecretService.new
|
237
|
+
new_service.api_key = nil
|
238
|
+
new_service.api_key
|
239
|
+
#=> nil
|
240
|
+
|
241
|
+
## Persistent field definitions are correctly identified
|
242
|
+
SecretService.field_types.values.select(&:persistent?).map(&:name).sort
|
243
|
+
#=> [:endpoint_url, :name, :service_id]
|
244
|
+
|
245
|
+
## Transient field definitions are correctly identified
|
246
|
+
transient_fields = SecretService.field_types.values.reject(&:persistent?).map(&:name).sort
|
247
|
+
transient_fields
|
248
|
+
#=> [:api_key, :password, :secret_token]
|
249
|
+
|
250
|
+
|
251
|
+
# TEARDOWN
|
252
|
+
|
253
|
+
# Clean up Database
|
254
|
+
@service.destroy! if @service.exists?
|
255
|
+
|
256
|
+
# Clean up any test objects
|
257
|
+
@service = nil
|
258
|
+
|
259
|
+
# Force garbage collection to trigger any finalizers
|
260
|
+
GC.start
|
data/try/helpers/test_helpers.rb
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
# e.g. FAMILIA_TRACE=1 FAMILIA_DEBUG=1 bundle exec try
|
6
6
|
|
7
7
|
require 'digest'
|
8
|
+
|
8
9
|
require_relative '../../lib/familia'
|
9
10
|
|
10
11
|
Familia.enable_database_logging = true
|
@@ -19,6 +20,8 @@ class Bone < Familia::Horreum
|
|
19
20
|
zset :metrics
|
20
21
|
hashkey :props
|
21
22
|
string :value, default: 'GREAT!'
|
23
|
+
counter :counter, default: 0
|
24
|
+
lock :lock
|
22
25
|
end
|
23
26
|
|
24
27
|
class Blone < Familia::Horreum
|
@@ -162,3 +165,67 @@ class Limiter < Familia::Horreum
|
|
162
165
|
@name
|
163
166
|
end
|
164
167
|
end
|
168
|
+
|
169
|
+
# # In test:
|
170
|
+
# using RedactedStringTestHelper
|
171
|
+
|
172
|
+
# secret = RedactedString.new("test-key")
|
173
|
+
# expect(secret.raw).to eq("test-key")
|
174
|
+
#
|
175
|
+
# Or with rack
|
176
|
+
#
|
177
|
+
# post '/vault' do
|
178
|
+
# passphrase = RedactedString.new(request.params['passphrase'])
|
179
|
+
# passphrase.expose do |plain|
|
180
|
+
# vault.unlock(plain)
|
181
|
+
# end
|
182
|
+
# # passphrase wiped
|
183
|
+
# end
|
184
|
+
#
|
185
|
+
# NOTE: This will do nothing unless RedactedString is already requried
|
186
|
+
unless defined?(RedactedString)
|
187
|
+
require_relative '../../lib/familia/features/transient_fields/redacted_string'
|
188
|
+
end
|
189
|
+
module RedactedStringTestHelper
|
190
|
+
refine RedactedString do
|
191
|
+
def raw
|
192
|
+
# Only available when refinement is used
|
193
|
+
@value
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
unless defined?(SingleUseRedactedString)
|
199
|
+
require_relative '../../lib/familia/features/transient_fields/single_use_redacted_string'
|
200
|
+
end
|
201
|
+
module SingleUseRedactedStringTestHelper
|
202
|
+
refine SingleUseRedactedString do
|
203
|
+
def raw
|
204
|
+
# Only available when refinement is used
|
205
|
+
@value
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# ConcealedString test helper for accessing encrypted values in tests
|
211
|
+
unless defined?(ConcealedString)
|
212
|
+
require_relative '../../lib/familia/features/encrypted_fields/concealed_string'
|
213
|
+
end
|
214
|
+
module ConcealedStringTestHelper
|
215
|
+
refine ConcealedString do
|
216
|
+
# TEST-ONLY: Direct access to decrypted value
|
217
|
+
#
|
218
|
+
# This method bypasses the reveal block pattern and directly returns
|
219
|
+
# the decrypted plaintext. It should ONLY be used in test environments
|
220
|
+
# through refinements to keep this dangerous method out of production.
|
221
|
+
#
|
222
|
+
# @return [String] The decrypted plaintext value
|
223
|
+
#
|
224
|
+
def reveal_for_testing
|
225
|
+
raise SecurityError, 'Encrypted data already cleared' if cleared?
|
226
|
+
raise SecurityError, 'No encrypted data to reveal' if @encrypted_data.nil?
|
227
|
+
|
228
|
+
@field_type.decrypt_value(@record, @encrypted_data)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
data/try/horreum/base_try.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
# try/horreum/base_try.rb
|
2
2
|
|
3
|
-
require_relative '../../lib/familia'
|
4
3
|
require_relative '../helpers/test_helpers'
|
5
4
|
|
6
5
|
Familia.debug = false
|
7
6
|
|
8
|
-
@identifier = 'tryouts-27@onetimesecret.
|
7
|
+
@identifier = 'tryouts-27@onetimesecret.dev'
|
9
8
|
@customer = Customer.new @identifier
|
10
9
|
@hashkey = Familia::HashKey.new 'tryouts-27'
|
11
10
|
|
@@ -55,7 +54,7 @@ Familia.debug = false
|
|
55
54
|
## Horreum object fields have a fast attribute method (1 of 2)
|
56
55
|
Familia.trace :LOAD, @customer.dbclient, @customer.uri, caller if Familia.debug?
|
57
56
|
@customer.name! 'Jane Doe'
|
58
|
-
#=>
|
57
|
+
#=> true
|
59
58
|
|
60
59
|
## Horreum object fields have a fast attribute method (2 of 2)
|
61
60
|
@customer.refresh!
|
@@ -116,3 +115,158 @@ class ArrayIdentifierTest < Familia::Horreum
|
|
116
115
|
field :name
|
117
116
|
end
|
118
117
|
#=!> Familia::Problem
|
118
|
+
|
119
|
+
## Redefining a field method after it can give a warning
|
120
|
+
class FieldRedefine < Familia::Horreum
|
121
|
+
identifier_field :email
|
122
|
+
field :name
|
123
|
+
field :uniquefieldname, on_conflict: :warn
|
124
|
+
|
125
|
+
def uniquefieldname
|
126
|
+
true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
#=2> /WARNING/
|
130
|
+
#=2> /uniquefieldname/
|
131
|
+
|
132
|
+
## Defining a field with the same name as an existing method can give a warning
|
133
|
+
class ::FieldRedefine2 < Familia::Horreum
|
134
|
+
identifier_field :email
|
135
|
+
field :name
|
136
|
+
|
137
|
+
def uniquefieldname
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
field :uniquefieldname, on_conflict: :warn
|
142
|
+
end
|
143
|
+
#=2> /WARNING/
|
144
|
+
#=2> /uniquefieldname/
|
145
|
+
|
146
|
+
## Redefining a field method after it can raise an error
|
147
|
+
class FieldRedefine3 < Familia::Horreum
|
148
|
+
identifier_field :email
|
149
|
+
field :name
|
150
|
+
field :uniquefieldname, on_conflict: :raise
|
151
|
+
|
152
|
+
def uniquefieldname
|
153
|
+
true
|
154
|
+
end
|
155
|
+
end
|
156
|
+
#=!> ArgumentError
|
157
|
+
|
158
|
+
## Defining a field with the same name as an existing method can raise an error
|
159
|
+
class FieldRedefine4 < Familia::Horreum
|
160
|
+
identifier_field :email
|
161
|
+
field :name
|
162
|
+
|
163
|
+
def uniquefieldname
|
164
|
+
true
|
165
|
+
end
|
166
|
+
|
167
|
+
field :uniquefieldname, on_conflict: :raise
|
168
|
+
end
|
169
|
+
#=!> ArgumentError
|
170
|
+
|
171
|
+
## Field aliasing works with 'as' parameter
|
172
|
+
class AliasedFieldTest < Familia::Horreum
|
173
|
+
identifier_field :email
|
174
|
+
field :email
|
175
|
+
field :display_size, as: :width
|
176
|
+
end
|
177
|
+
@aliased = AliasedFieldTest.new email: 'test@example.com'
|
178
|
+
@aliased.width = 42
|
179
|
+
@aliased.width
|
180
|
+
#=> 42
|
181
|
+
|
182
|
+
## Aliased field getter method uses alias name
|
183
|
+
@aliased.respond_to?(:width)
|
184
|
+
#=> true
|
185
|
+
|
186
|
+
## Aliased field setter method uses alias name
|
187
|
+
@aliased.respond_to?(:width=)
|
188
|
+
#=> true
|
189
|
+
|
190
|
+
## Original field name is not accessible as method
|
191
|
+
@aliased.respond_to?(:display_size)
|
192
|
+
#=> false
|
193
|
+
|
194
|
+
## Aliased field fast method works correctly
|
195
|
+
@aliased.save
|
196
|
+
@aliased.display_size! 100
|
197
|
+
#=> true
|
198
|
+
|
199
|
+
## Aliased field refresh works correctly
|
200
|
+
@aliased.width = 50 # unsaved change
|
201
|
+
@aliased.refresh!
|
202
|
+
@aliased.width
|
203
|
+
#=> "100"
|
204
|
+
|
205
|
+
## Fast method with custom name
|
206
|
+
class CustomFastMethodTest < Familia::Horreum
|
207
|
+
identifier_field :email
|
208
|
+
field :score, fast_method: :score_now!
|
209
|
+
field :email
|
210
|
+
end
|
211
|
+
@custom_fast = CustomFastMethodTest.new email: 'fast@example.com'
|
212
|
+
@custom_fast.respond_to?(:score_now!)
|
213
|
+
#=> true
|
214
|
+
|
215
|
+
## Custom fast method works
|
216
|
+
@custom_fast.save
|
217
|
+
@custom_fast.score_now! 75
|
218
|
+
#=> true
|
219
|
+
|
220
|
+
## Field with :warn conflict handling allows redefinition with warning
|
221
|
+
class WarnConflictTest < Familia::Horreum
|
222
|
+
identifier_field :email
|
223
|
+
field :email
|
224
|
+
field :test_method, on_conflict: :warn
|
225
|
+
|
226
|
+
def test_method
|
227
|
+
"original"
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
@warn_test = WarnConflictTest.new email: 'warn@example.com'
|
232
|
+
@warn_test.test_method
|
233
|
+
#=> "original"
|
234
|
+
|
235
|
+
## Field with :skip conflict handling skips redefinition silently
|
236
|
+
class SkipConflictTest < Familia::Horreum
|
237
|
+
identifier_field :email
|
238
|
+
field :email
|
239
|
+
|
240
|
+
def skip_method
|
241
|
+
"original"
|
242
|
+
end
|
243
|
+
|
244
|
+
field :skip_method, on_conflict: :skip
|
245
|
+
end
|
246
|
+
@skip_test = SkipConflictTest.new email: 'skip@example.com'
|
247
|
+
@skip_test.skip_method
|
248
|
+
#=> "original"
|
249
|
+
|
250
|
+
## Combined aliasing and custom fast method
|
251
|
+
class CombinedTest < Familia::Horreum
|
252
|
+
identifier_field :email
|
253
|
+
field :internal_count, as: :count, fast_method: :count_immediately!
|
254
|
+
field :email
|
255
|
+
end
|
256
|
+
@combined = CombinedTest.new email: 'combined1@example.com'
|
257
|
+
@combined.count = 10
|
258
|
+
@combined.count
|
259
|
+
#=> 10
|
260
|
+
|
261
|
+
## Combined test fast method works
|
262
|
+
@combined.save
|
263
|
+
@combined.count_immediately! 20
|
264
|
+
@combined.count
|
265
|
+
#=> 20
|
266
|
+
|
267
|
+
## Combined test refresh works
|
268
|
+
combined = CombinedTest.new email: 'combined2@example.com'
|
269
|
+
combined.count = 5 # unsaved change
|
270
|
+
combined.refresh!
|
271
|
+
combined.count
|
272
|
+
#=> 5
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# try/horreum/enhanced_conflict_handling_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
Familia.debug = false
|
6
|
+
|
7
|
+
## Valid strategies are defined correctly
|
8
|
+
Familia::VALID_STRATEGIES.include?(:raise)
|
9
|
+
#=> true
|
10
|
+
|
11
|
+
## Valid strategies include all expected options
|
12
|
+
Familia::VALID_STRATEGIES
|
13
|
+
#=> [:raise, :skip, :ignore, :warn, :overwrite]
|
14
|
+
|
15
|
+
## Overwrite strategy removes existing method and defines new one
|
16
|
+
class OverwriteStrategyTest < Familia::Horreum
|
17
|
+
identifier_field :id
|
18
|
+
field :id
|
19
|
+
|
20
|
+
def conflicting_method
|
21
|
+
"original_method"
|
22
|
+
end
|
23
|
+
|
24
|
+
field :conflicting_method, on_conflict: :overwrite
|
25
|
+
end
|
26
|
+
@overwrite_test = OverwriteStrategyTest.new(id: 'overwrite1')
|
27
|
+
@overwrite_test.conflicting_method = "new_value"
|
28
|
+
@overwrite_test.conflicting_method
|
29
|
+
#=> "new_value"
|
30
|
+
|
31
|
+
## Overwrite strategy works with fast methods too
|
32
|
+
@overwrite_test.save
|
33
|
+
@overwrite_test.conflicting_method! "fast_value"
|
34
|
+
#=> true
|
35
|
+
|
36
|
+
## Invalid conflict strategy raises error during field definition
|
37
|
+
class InvalidStrategyTest < Familia::Horreum
|
38
|
+
identifier_field :id
|
39
|
+
field :id
|
40
|
+
field :test_field, on_conflict: :invalid_strategy
|
41
|
+
end
|
42
|
+
#=!> ArgumentError
|
43
|
+
|
44
|
+
## Method conflict detection works with instance methods
|
45
|
+
class ConflictDetectionTest < Familia::Horreum
|
46
|
+
identifier_field :id
|
47
|
+
field :id
|
48
|
+
|
49
|
+
def existing_method
|
50
|
+
"exists"
|
51
|
+
end
|
52
|
+
|
53
|
+
field :existing_method, on_conflict: :raise
|
54
|
+
end
|
55
|
+
#=!> ArgumentError
|
56
|
+
|
57
|
+
## Conflict detection provides helpful error message
|
58
|
+
begin
|
59
|
+
class ConflictMessageTest < Familia::Horreum
|
60
|
+
identifier_field :id
|
61
|
+
field :id
|
62
|
+
|
63
|
+
def another_method
|
64
|
+
"exists"
|
65
|
+
end
|
66
|
+
|
67
|
+
field :another_method, on_conflict: :raise
|
68
|
+
end
|
69
|
+
rescue ArgumentError => e
|
70
|
+
e.message.include?("another_method")
|
71
|
+
end
|
72
|
+
#=> true
|
73
|
+
|
74
|
+
## Method location information in error message when possible
|
75
|
+
|
76
|
+
class LocationInfoTest < Familia::Horreum
|
77
|
+
identifier_field :id
|
78
|
+
field :id
|
79
|
+
|
80
|
+
def location_test_method
|
81
|
+
"exists"
|
82
|
+
end
|
83
|
+
|
84
|
+
field :location_test_method, on_conflict: :raise
|
85
|
+
end
|
86
|
+
#=!> ArgumentError
|
87
|
+
#==> error.message.include?("already defined")
|
88
|
+
|
89
|
+
## Skip strategy silently ignores conflicts
|
90
|
+
class SkipStrategyTest < Familia::Horreum
|
91
|
+
identifier_field :id
|
92
|
+
field :id
|
93
|
+
|
94
|
+
def skip_method
|
95
|
+
"original"
|
96
|
+
end
|
97
|
+
|
98
|
+
field :skip_method, on_conflict: :skip
|
99
|
+
end
|
100
|
+
@skip_test = SkipStrategyTest.new(id: 'skip1')
|
101
|
+
@skip_test.skip_method
|
102
|
+
#=> "original"
|
103
|
+
|
104
|
+
## Skip strategy doesn't create accessor methods when method exists
|
105
|
+
@skip_test.respond_to?(:skip_method=)
|
106
|
+
#=> false
|
107
|
+
|
108
|
+
## Warn strategy shows warning but continues with definition
|
109
|
+
class WarnStrategyTest < Familia::Horreum
|
110
|
+
identifier_field :id
|
111
|
+
field :id
|
112
|
+
|
113
|
+
def warn_method
|
114
|
+
"original"
|
115
|
+
end
|
116
|
+
|
117
|
+
field :warn_method, on_conflict: :warn
|
118
|
+
end
|
119
|
+
#=2> /WARNING/
|
120
|
+
@warn_test = WarnStrategyTest.new(id: 'warn1')
|
121
|
+
@warn_test.warn_method = "new_value"
|
122
|
+
@warn_test.warn_method
|
123
|
+
#=> "new_value"
|
124
|
+
|
125
|
+
## Fast method names must end with exclamation mark
|
126
|
+
class InvalidFastMethodTest < Familia::Horreum
|
127
|
+
identifier_field :id
|
128
|
+
field :id
|
129
|
+
field :test_field, fast_method: :invalid_name
|
130
|
+
end
|
131
|
+
#=!> ArgumentError
|
132
|
+
|
133
|
+
## Fast method validation works with custom names
|
134
|
+
class ValidFastMethodTest < Familia::Horreum
|
135
|
+
identifier_field :id
|
136
|
+
field :id
|
137
|
+
field :score, fast_method: :update_score_now!
|
138
|
+
end
|
139
|
+
@valid_fast = ValidFastMethodTest.new(id: 'valid1')
|
140
|
+
@valid_fast.respond_to?(:update_score_now!)
|
141
|
+
#=> true
|
142
|
+
|
143
|
+
## Method added hook detects conflicts after field definition
|
144
|
+
class MethodAddedHookTest < Familia::Horreum
|
145
|
+
identifier_field :id
|
146
|
+
field :id
|
147
|
+
field :hook_test, on_conflict: :warn
|
148
|
+
|
149
|
+
def hook_test
|
150
|
+
"redefined_after"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
#=2> /WARNING/
|
154
|
+
#=2> /hook_test/
|
155
|
+
#=2> /redefined after field definition/
|
156
|
+
|
157
|
+
## Method added hook works with raise strategy too
|
158
|
+
class MethodAddedRaiseTest < Familia::Horreum
|
159
|
+
identifier_field :id
|
160
|
+
field :id
|
161
|
+
field :raise_hook_test, on_conflict: :raise
|
162
|
+
|
163
|
+
def raise_hook_test
|
164
|
+
"redefined"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
#=!> ArgumentError
|
168
|
+
|
169
|
+
@overwrite_test.destroy! rescue nil
|
170
|
+
@skip_test.destroy! rescue nil
|
171
|
+
@warn_test.destroy! rescue nil
|
172
|
+
@valid_fast.destroy! rescue nil
|
173
|
+
@overwrite_test = nil
|
174
|
+
@skip_test = nil
|
175
|
+
@warn_test = nil
|
176
|
+
@valid_fast = nil
|