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,248 @@
|
|
1
|
+
# try/features/transient_fields/redacted_string_try.rb
|
2
|
+
|
3
|
+
require_relative '../../helpers/test_helpers'
|
4
|
+
|
5
|
+
|
6
|
+
# Create sample sensitive values for testing
|
7
|
+
@api_key = "sk-1234567890abcdef"
|
8
|
+
@password = "super_secret_password_123!"
|
9
|
+
@empty_secret = ""
|
10
|
+
@long_secret = "a" * 100 # Test long string handling
|
11
|
+
@special_chars = "päßwörd!@#$%^&*()"
|
12
|
+
|
13
|
+
## TEST CASES
|
14
|
+
|
15
|
+
## Basic initialization creates RedactedString instance
|
16
|
+
redacted = RedactedString.new(@api_key)
|
17
|
+
redacted.class
|
18
|
+
#=> RedactedString
|
19
|
+
|
20
|
+
## Initialization accepts various input types
|
21
|
+
RedactedString.new("string").class
|
22
|
+
#=> RedactedString
|
23
|
+
|
24
|
+
RedactedString.new(123).class # to_s conversion
|
25
|
+
#=> RedactedString
|
26
|
+
|
27
|
+
RedactedString.new(nil).class # nil handling
|
28
|
+
#=> RedactedString
|
29
|
+
|
30
|
+
## Empty string handling
|
31
|
+
empty_redacted = RedactedString.new(@empty_secret)
|
32
|
+
empty_redacted.class
|
33
|
+
#=> RedactedString
|
34
|
+
|
35
|
+
## Long string handling
|
36
|
+
long_redacted = RedactedString.new(@long_secret)
|
37
|
+
long_redacted.class
|
38
|
+
#=> RedactedString
|
39
|
+
|
40
|
+
## Special characters handling
|
41
|
+
special_redacted = RedactedString.new(@special_chars)
|
42
|
+
special_redacted.class
|
43
|
+
#=> RedactedString
|
44
|
+
|
45
|
+
## Fresh instance is not cleared initially
|
46
|
+
fresh_redacted = RedactedString.new(@api_key)
|
47
|
+
fresh_redacted.cleared?
|
48
|
+
#=> false
|
49
|
+
|
50
|
+
## to_s always returns redacted placeholder
|
51
|
+
redacted_for_to_s = RedactedString.new(@api_key)
|
52
|
+
redacted_for_to_s.to_s
|
53
|
+
#=> "[REDACTED]"
|
54
|
+
|
55
|
+
## inspect returns same as to_s for security
|
56
|
+
redacted_for_inspect = RedactedString.new(@password)
|
57
|
+
redacted_for_inspect.inspect
|
58
|
+
#=> "[REDACTED]"
|
59
|
+
|
60
|
+
## String interpolation is redacted
|
61
|
+
redacted_for_interpolation = RedactedString.new(@api_key)
|
62
|
+
"Token: #{redacted_for_interpolation}"
|
63
|
+
#=> "Token: [REDACTED]"
|
64
|
+
|
65
|
+
## Array/Hash containing redacted strings show redacted values
|
66
|
+
redacted_in_array = RedactedString.new(@password)
|
67
|
+
[redacted_in_array].to_s.include?("[REDACTED]")
|
68
|
+
#=> true
|
69
|
+
|
70
|
+
## expose method requires block
|
71
|
+
redacted_for_expose_check = RedactedString.new(@api_key)
|
72
|
+
begin
|
73
|
+
redacted_for_expose_check.expose
|
74
|
+
rescue ArgumentError => e
|
75
|
+
e.message
|
76
|
+
end
|
77
|
+
#=> "Block required"
|
78
|
+
|
79
|
+
## expose method provides access to original value
|
80
|
+
redacted_for_expose = RedactedString.new("sk-1234567890abcdef")
|
81
|
+
result = nil
|
82
|
+
redacted_for_expose.expose { |val| result = val.dup }
|
83
|
+
result
|
84
|
+
#=> "sk-1234567890abcdef"
|
85
|
+
|
86
|
+
## expose method does not automatically clear after use
|
87
|
+
redacted_single_use = RedactedString.new(@password)
|
88
|
+
redacted_single_use.expose { |val| val.length }
|
89
|
+
redacted_single_use.cleared?
|
90
|
+
#=> false
|
91
|
+
|
92
|
+
## expose method does not clear if exception occurs
|
93
|
+
redacted_exception_test = RedactedString.new(@api_key)
|
94
|
+
begin
|
95
|
+
redacted_exception_test.expose { |val| raise "test error" }
|
96
|
+
rescue => e
|
97
|
+
# Exception occurred, but string should still be cleared
|
98
|
+
end
|
99
|
+
redacted_exception_test.cleared?
|
100
|
+
#=> false
|
101
|
+
|
102
|
+
## expose method on cleared string raises SecurityError
|
103
|
+
cleared_redacted = RedactedString.new(@password)
|
104
|
+
cleared_redacted.clear!
|
105
|
+
begin
|
106
|
+
cleared_redacted.expose { |val| val }
|
107
|
+
rescue SecurityError => e
|
108
|
+
e.message
|
109
|
+
end
|
110
|
+
#=> "Value already cleared"
|
111
|
+
|
112
|
+
## clear! method marks string as cleared
|
113
|
+
redacted_for_clear = RedactedString.new(@api_key)
|
114
|
+
redacted_for_clear.clear!
|
115
|
+
redacted_for_clear.cleared?
|
116
|
+
#=> true
|
117
|
+
|
118
|
+
## clear! method is safe to call multiple times
|
119
|
+
redacted_multi_clear = RedactedString.new(@password)
|
120
|
+
redacted_multi_clear.clear!
|
121
|
+
redacted_multi_clear.clear! # Second call
|
122
|
+
redacted_multi_clear.cleared?
|
123
|
+
#=> true
|
124
|
+
|
125
|
+
## clear! method freezes the object
|
126
|
+
redacted_freeze_test = RedactedString.new(@api_key)
|
127
|
+
redacted_freeze_test.clear!
|
128
|
+
redacted_freeze_test.frozen?
|
129
|
+
#=> true
|
130
|
+
|
131
|
+
## Equality comparison only true for same object (prevents timing attacks)
|
132
|
+
redacted1 = RedactedString.new(@api_key)
|
133
|
+
redacted2 = RedactedString.new(@api_key)
|
134
|
+
redacted1 == redacted2
|
135
|
+
#=> false
|
136
|
+
|
137
|
+
## Same object equality returns true
|
138
|
+
redacted_same = RedactedString.new(@password)
|
139
|
+
redacted_same == redacted_same
|
140
|
+
#=> true
|
141
|
+
|
142
|
+
## eql? behaves same as ==
|
143
|
+
redacted_eql1 = RedactedString.new(@api_key)
|
144
|
+
redacted_eql2 = RedactedString.new(@api_key)
|
145
|
+
redacted_eql1.eql?(redacted_eql2)
|
146
|
+
#=> false
|
147
|
+
|
148
|
+
## Same object eql? returns true
|
149
|
+
redacted_eql_same = RedactedString.new(@password)
|
150
|
+
redacted_eql_same.eql?(redacted_eql_same)
|
151
|
+
#=> true
|
152
|
+
|
153
|
+
## All instances have same hash (prevents hash-based timing attacks)
|
154
|
+
redacted_hash1 = RedactedString.new(@api_key)
|
155
|
+
redacted_hash2 = RedactedString.new(@password)
|
156
|
+
redacted_hash1.hash == redacted_hash2.hash
|
157
|
+
#=> true
|
158
|
+
|
159
|
+
## Hash value is consistent with class hash
|
160
|
+
redacted_hash_consistent = RedactedString.new(@api_key)
|
161
|
+
redacted_hash_consistent.hash == RedactedString.hash
|
162
|
+
#=> true
|
163
|
+
|
164
|
+
## RedactedString cannot be used in string operations without expose
|
165
|
+
redacted_no_concat = RedactedString.new(@api_key)
|
166
|
+
begin
|
167
|
+
result = redacted_no_concat + "suffix"
|
168
|
+
false # Should not reach here
|
169
|
+
rescue => e
|
170
|
+
true # Expected to raise error
|
171
|
+
end
|
172
|
+
#=> true
|
173
|
+
|
174
|
+
## RedactedString is not a String subclass (security by design)
|
175
|
+
redacted_type_check = RedactedString.new(@password)
|
176
|
+
redacted_type_check.is_a?(String)
|
177
|
+
#=> false
|
178
|
+
|
179
|
+
## Working with empty strings
|
180
|
+
empty_redacted_test = RedactedString.new("")
|
181
|
+
result = nil
|
182
|
+
empty_redacted_test.expose { |val| result = val }
|
183
|
+
result
|
184
|
+
#=> ""
|
185
|
+
|
186
|
+
## Working with long strings preserves content
|
187
|
+
long_redacted_test = RedactedString.new("a" * 100)
|
188
|
+
result = nil
|
189
|
+
long_redacted_test.expose { |val| result = val.length }
|
190
|
+
result
|
191
|
+
#=> 100
|
192
|
+
|
193
|
+
## Special characters are preserved
|
194
|
+
special_redacted_test = RedactedString.new("päßwörd!@#$%^&*()")
|
195
|
+
result = nil
|
196
|
+
special_redacted_test.expose { |val| result = val.dup }
|
197
|
+
result
|
198
|
+
#=> "päßwörd!@#$%^&*()"
|
199
|
+
|
200
|
+
## Finalizer proc exists and is callable
|
201
|
+
RedactedString.finalizer_proc.class
|
202
|
+
#=> Proc
|
203
|
+
|
204
|
+
## Cleared redacted string maintains redacted appearance
|
205
|
+
cleared_appearance_test = RedactedString.new(@api_key)
|
206
|
+
cleared_appearance_test.clear!
|
207
|
+
cleared_appearance_test.to_s
|
208
|
+
#=> "[REDACTED]"
|
209
|
+
|
210
|
+
## Cleared redacted string inspect still redacted
|
211
|
+
cleared_inspect_test = RedactedString.new(@password)
|
212
|
+
cleared_inspect_test.clear!
|
213
|
+
cleared_inspect_test.inspect
|
214
|
+
#=> "[REDACTED]"
|
215
|
+
|
216
|
+
## Object created from nil input
|
217
|
+
nil_input_test = RedactedString.new(nil)
|
218
|
+
result = nil
|
219
|
+
nil_input_test.expose { |val| result = val.dup }
|
220
|
+
result
|
221
|
+
#=> ""
|
222
|
+
|
223
|
+
## Numeric input converted to string
|
224
|
+
numeric_input_test = RedactedString.new(42)
|
225
|
+
result = nil
|
226
|
+
numeric_input_test.expose { |val| result = val.dup }
|
227
|
+
result
|
228
|
+
#=> "42"
|
229
|
+
|
230
|
+
## Symbol input converted to string
|
231
|
+
symbol_input_test = RedactedString.new(:secret)
|
232
|
+
result = nil
|
233
|
+
symbol_input_test.expose { |val| result = val.dup }
|
234
|
+
result
|
235
|
+
#=> "secret"
|
236
|
+
|
237
|
+
|
238
|
+
# TEARDOWN
|
239
|
+
|
240
|
+
# Clean up any remaining test objects
|
241
|
+
@api_key = nil
|
242
|
+
@password = nil
|
243
|
+
@empty_secret = nil
|
244
|
+
@long_secret = nil
|
245
|
+
@special_chars = nil
|
246
|
+
|
247
|
+
# Force garbage collection to trigger any finalizers
|
248
|
+
GC.start
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# try/features/transient_fields/refresh_reset_try.rb
|
2
|
+
# Test that refresh! properly resets transient fields to nil
|
3
|
+
|
4
|
+
require_relative '../../helpers/test_helpers'
|
5
|
+
|
6
|
+
Familia.debug = false
|
7
|
+
|
8
|
+
Familia.dbclient.flushdb
|
9
|
+
|
10
|
+
class SecretService < Familia::Horreum
|
11
|
+
identifier_field :name
|
12
|
+
|
13
|
+
field :name
|
14
|
+
field :endpoint_url
|
15
|
+
|
16
|
+
transient_field :api_key
|
17
|
+
transient_field :password
|
18
|
+
transient_field :secret_token, as: :token
|
19
|
+
end
|
20
|
+
|
21
|
+
@service = SecretService.new
|
22
|
+
@service.name = 'test-service'
|
23
|
+
@service.endpoint_url = 'https://api.example.com'
|
24
|
+
@service.api_key = 'sk-1234567890abcdef'
|
25
|
+
@service.password = 'super-secret-password'
|
26
|
+
@service.token = 'token-xyz789'
|
27
|
+
|
28
|
+
|
29
|
+
## Verify class has the expected fields
|
30
|
+
SecretService.fields.sort
|
31
|
+
#=> [:api_key, :endpoint_url, :name, :password, :secret_token]
|
32
|
+
|
33
|
+
## Verify service was created successfully
|
34
|
+
@service.nil?
|
35
|
+
#=> false
|
36
|
+
|
37
|
+
## Save persistent fields to database
|
38
|
+
@service.save
|
39
|
+
#=> true
|
40
|
+
|
41
|
+
## Verify transient fields have values before refresh
|
42
|
+
@service.api_key.nil?
|
43
|
+
#=> false
|
44
|
+
|
45
|
+
## Verify transient fields are RedactedString instances
|
46
|
+
@service.api_key
|
47
|
+
#=:> RedactedString
|
48
|
+
|
49
|
+
## Verify transient fields will not expose the value like a string
|
50
|
+
@service.api_key.to_s
|
51
|
+
#=> '[REDACTED]'
|
52
|
+
|
53
|
+
## Verify transient fields will expose the value when asked
|
54
|
+
@service.api_key.value
|
55
|
+
#=> 'sk-1234567890abcdef'
|
56
|
+
|
57
|
+
## Verify password field has value before refresh
|
58
|
+
@service.password.nil?
|
59
|
+
#=> false
|
60
|
+
|
61
|
+
## Verify token alias has value before refresh
|
62
|
+
@service.token.nil?
|
63
|
+
#=> false
|
64
|
+
|
65
|
+
## Verify persistent fields have values before refresh
|
66
|
+
@service.name
|
67
|
+
#=> "test-service"
|
68
|
+
|
69
|
+
## Verify endpoint_url has value before refresh
|
70
|
+
@service.endpoint_url
|
71
|
+
#=> "https://api.example.com"
|
72
|
+
|
73
|
+
## Refresh! should reset transient fields to nil but keep persistent ones
|
74
|
+
@service.refresh!
|
75
|
+
#=> [:name, :endpoint_url]
|
76
|
+
|
77
|
+
## After refresh!, transient fields should be nil
|
78
|
+
@service.api_key.nil?
|
79
|
+
#=> true
|
80
|
+
|
81
|
+
## After refresh!, password should be nil
|
82
|
+
@service.password.nil?
|
83
|
+
#=> true
|
84
|
+
|
85
|
+
## After refresh!, token alias should be nil
|
86
|
+
@service.token.nil?
|
87
|
+
#=> true
|
88
|
+
|
89
|
+
## After refresh!, persistent fields should retain their values
|
90
|
+
@service.name
|
91
|
+
#=> "test-service"
|
92
|
+
|
93
|
+
## After refresh!, endpoint_url should retain its value
|
94
|
+
@service.endpoint_url
|
95
|
+
#=> "https://api.example.com"
|
96
|
+
|
97
|
+
## Set transient fields again after refresh
|
98
|
+
@service.api_key = 'new-api-key-after-refresh'
|
99
|
+
@service.password = 'new-password-after-refresh'
|
100
|
+
@service.token = 'new-token-after-refresh'
|
101
|
+
#=> 'new-token-after-refresh'
|
102
|
+
|
103
|
+
## Verify transient fields have new values
|
104
|
+
@service.api_key.nil?
|
105
|
+
#=> false
|
106
|
+
|
107
|
+
## Verify they're still RedactedString instances
|
108
|
+
@service.api_key
|
109
|
+
#=:> RedactedString
|
110
|
+
|
111
|
+
## Another refresh! should reset them again
|
112
|
+
@service.refresh!
|
113
|
+
#=> [:name, :endpoint_url]
|
114
|
+
|
115
|
+
## Transient fields should be nil again
|
116
|
+
@service.api_key.nil?
|
117
|
+
#=> true
|
118
|
+
|
119
|
+
## Password should be nil again
|
120
|
+
@service.password.nil?
|
121
|
+
#=> true
|
122
|
+
|
123
|
+
## Token should be nil again
|
124
|
+
@service.token.nil?
|
125
|
+
#=> true
|
126
|
+
|
127
|
+
## But persistent fields should remain intact
|
128
|
+
@service.name
|
129
|
+
#=> "test-service"
|
130
|
+
|
131
|
+
## Endpoint URL should remain intact
|
132
|
+
@service.endpoint_url
|
133
|
+
#=> "https://api.example.com"
|
134
|
+
|
135
|
+
## Test refresh! with object that has no transient fields
|
136
|
+
class SimpleService < Familia::Horreum
|
137
|
+
identifier_field :id
|
138
|
+
field :id
|
139
|
+
field :name
|
140
|
+
field :status
|
141
|
+
end
|
142
|
+
|
143
|
+
@no_transient = SimpleService.new('no-transient-test')
|
144
|
+
@no_transient.name = 'No Transient Service'
|
145
|
+
@no_transient.status = 'active'
|
146
|
+
|
147
|
+
# Save and refresh should work normally without transient fields
|
148
|
+
@no_transient.save
|
149
|
+
@no_transient.refresh!
|
150
|
+
|
151
|
+
# All fields should retain their values
|
152
|
+
@no_transient.id
|
153
|
+
#=> "no-transient-test"
|
154
|
+
|
155
|
+
## Name should be preserved
|
156
|
+
@no_transient.name
|
157
|
+
#=> "No Transient Service"
|
158
|
+
|
159
|
+
## Status should be preserved
|
160
|
+
@no_transient.status
|
161
|
+
#=> "active"
|
162
|
+
|
163
|
+
|
164
|
+
[@service, @no_transient].each(&:destroy!)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
|
2
|
+
require_relative '../../helpers/test_helpers'
|
3
|
+
|
4
|
+
Familia.debug = false
|
5
|
+
Familia.dbclient.flushdb
|
6
|
+
|
7
|
+
# Use existing Customer class (should work)
|
8
|
+
service = Customer.new('refresh-test-customer')
|
9
|
+
puts "Created customer: #{service.class}"
|
10
|
+
puts "Customer identifier: #{service.identifier}"
|
11
|
+
|
12
|
+
# Add a transient field for testing
|
13
|
+
class Customer
|
14
|
+
transient_field :temp_data
|
15
|
+
end
|
16
|
+
|
17
|
+
# Set some values
|
18
|
+
service.name = 'Test Customer'
|
19
|
+
service.temp_data = 'secret-info'
|
20
|
+
|
21
|
+
puts "Before save:"
|
22
|
+
puts " name: #{service.name.inspect}"
|
23
|
+
puts " temp_data: #{service.temp_data.inspect}"
|
24
|
+
puts " temp_data class: #{service.temp_data.class}"
|
25
|
+
|
26
|
+
# Save to database
|
27
|
+
result = service.save
|
28
|
+
puts "Save result: #{result}"
|
29
|
+
|
30
|
+
puts "Before refresh:"
|
31
|
+
puts " name: #{service.name.inspect}"
|
32
|
+
puts " temp_data: #{service.temp_data.inspect}"
|
33
|
+
puts " temp_data nil?: #{service.temp_data.nil?}"
|
34
|
+
|
35
|
+
# Refresh should reset transient field to nil but keep persistent field
|
36
|
+
service.refresh!
|
37
|
+
puts "After refresh:"
|
38
|
+
puts " name: #{service.name.inspect}"
|
39
|
+
puts " temp_data: #{service.temp_data.inspect}"
|
40
|
+
puts " temp_data nil?: #{service.temp_data.nil?}"
|
41
|
+
|
42
|
+
# Verify that the refresh! reset worked as expected
|
43
|
+
if service.temp_data.nil? && service.name == 'Test Customer'
|
44
|
+
puts "SUCCESS: refresh! properly reset transient field while preserving persistent field"
|
45
|
+
else
|
46
|
+
puts "FAILED: refresh! did not work as expected"
|
47
|
+
end
|
48
|
+
|
49
|
+
service.destroy!
|
50
|
+
puts "Test completed"
|