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,310 @@
|
|
1
|
+
# try/features/transient_fields/single_use_redacted_string_try.rb
|
2
|
+
|
3
|
+
require_relative '../../helpers/test_helpers'
|
4
|
+
|
5
|
+
@otp_code = "123456"
|
6
|
+
@auth_token = "temp-auth-token-xyz"
|
7
|
+
@encryption_key = "encryption-key-abc123"
|
8
|
+
@empty_secret = ""
|
9
|
+
@long_secret = "x" * 50 # Test long string handling
|
10
|
+
@special_chars = "tëmp!@#$%"
|
11
|
+
|
12
|
+
## Basic initialization creates SingleUseRedactedString instance
|
13
|
+
single_use = SingleUseRedactedString.new(@otp_code)
|
14
|
+
single_use.class
|
15
|
+
#=> SingleUseRedactedString
|
16
|
+
|
17
|
+
## SingleUseRedactedString inherits from RedactedString
|
18
|
+
single_use_inheritance = SingleUseRedactedString.new(@auth_token)
|
19
|
+
single_use_inheritance.is_a?(RedactedString)
|
20
|
+
#=> true
|
21
|
+
|
22
|
+
## Initialization accepts various input types
|
23
|
+
SingleUseRedactedString.new("string").class
|
24
|
+
#=> SingleUseRedactedString
|
25
|
+
|
26
|
+
SingleUseRedactedString.new(123).class # to_s conversion
|
27
|
+
#=> SingleUseRedactedString
|
28
|
+
|
29
|
+
SingleUseRedactedString.new(nil).class # nil handling
|
30
|
+
#=> SingleUseRedactedString
|
31
|
+
|
32
|
+
## Fresh instance is not cleared initially
|
33
|
+
fresh_single_use = SingleUseRedactedString.new(@otp_code)
|
34
|
+
fresh_single_use.cleared?
|
35
|
+
#=> false
|
36
|
+
|
37
|
+
## to_s returns redacted placeholder (inherited behavior)
|
38
|
+
single_use_to_s = SingleUseRedactedString.new(@auth_token)
|
39
|
+
single_use_to_s.to_s
|
40
|
+
#=> "[REDACTED]"
|
41
|
+
|
42
|
+
## inspect returns same as to_s (inherited behavior)
|
43
|
+
single_use_inspect = SingleUseRedactedString.new(@encryption_key)
|
44
|
+
single_use_inspect.inspect
|
45
|
+
#=> "[REDACTED]"
|
46
|
+
|
47
|
+
## String interpolation is redacted (inherited behavior)
|
48
|
+
single_use_interpolation = SingleUseRedactedString.new(@otp_code)
|
49
|
+
"OTP: #{single_use_interpolation}"
|
50
|
+
#=> "OTP: [REDACTED]"
|
51
|
+
|
52
|
+
## Direct value() access raises SecurityError (overridden behavior)
|
53
|
+
single_use_direct_access = SingleUseRedactedString.new(@auth_token)
|
54
|
+
begin
|
55
|
+
single_use_direct_access.value
|
56
|
+
rescue SecurityError => e
|
57
|
+
e.message
|
58
|
+
end
|
59
|
+
#=> "Direct value access not allowed for single-use secrets. Use #expose with a block."
|
60
|
+
|
61
|
+
## expose method requires block (inherited behavior)
|
62
|
+
single_use_no_block = SingleUseRedactedString.new(@otp_code)
|
63
|
+
begin
|
64
|
+
single_use_no_block.expose
|
65
|
+
rescue ArgumentError => e
|
66
|
+
e.message
|
67
|
+
end
|
68
|
+
#=> "Block required"
|
69
|
+
|
70
|
+
## expose method provides access to original value
|
71
|
+
single_use_expose = SingleUseRedactedString.new("123456")
|
72
|
+
result = nil
|
73
|
+
single_use_expose.expose { |val| result = val.dup }
|
74
|
+
result
|
75
|
+
#=> "123456"
|
76
|
+
|
77
|
+
## expose automatically clears value after use (key single-use behavior)
|
78
|
+
single_use_auto_clear = SingleUseRedactedString.new(@otp_code)
|
79
|
+
single_use_auto_clear.expose { |val| val.length }
|
80
|
+
single_use_auto_clear.cleared?
|
81
|
+
#=> true
|
82
|
+
|
83
|
+
## Second expose attempt raises SecurityError after clearing
|
84
|
+
single_use_second_expose = SingleUseRedactedString.new(@auth_token)
|
85
|
+
single_use_second_expose.expose { |val| val.upcase } # First use
|
86
|
+
begin
|
87
|
+
single_use_second_expose.expose { |val| val } # Second attempt
|
88
|
+
rescue SecurityError => e
|
89
|
+
e.message
|
90
|
+
end
|
91
|
+
#=> "Value already cleared"
|
92
|
+
|
93
|
+
## expose clears value even when exception occurs in block
|
94
|
+
single_use_exception = SingleUseRedactedString.new(@encryption_key)
|
95
|
+
begin
|
96
|
+
single_use_exception.expose { |val| raise "test error" }
|
97
|
+
rescue => e
|
98
|
+
# Exception occurred, but string should still be cleared
|
99
|
+
end
|
100
|
+
single_use_exception.cleared?
|
101
|
+
#=> true
|
102
|
+
|
103
|
+
## expose clears value even when block returns early
|
104
|
+
single_use_early_return = SingleUseRedactedString.new(@otp_code)
|
105
|
+
result = single_use_early_return.expose do |val|
|
106
|
+
next "early" if val.length > 0
|
107
|
+
"normal"
|
108
|
+
end
|
109
|
+
single_use_early_return.cleared?
|
110
|
+
#=> true
|
111
|
+
|
112
|
+
## Multiple values can be processed in the expose block
|
113
|
+
single_use_multiple_ops = SingleUseRedactedString.new("password123")
|
114
|
+
results = []
|
115
|
+
single_use_multiple_ops.expose do |val|
|
116
|
+
results << val.length
|
117
|
+
results << val.upcase
|
118
|
+
results << val.include?("123")
|
119
|
+
end
|
120
|
+
results
|
121
|
+
#=> [11, "PASSWORD123", true]
|
122
|
+
|
123
|
+
## expose with empty string
|
124
|
+
single_use_empty = SingleUseRedactedString.new(@empty_secret)
|
125
|
+
result = nil
|
126
|
+
single_use_empty.expose { |val| result = val.dup }
|
127
|
+
result
|
128
|
+
#=> ""
|
129
|
+
|
130
|
+
## Can manually expose the length of the value without duplicating
|
131
|
+
single_use_long = SingleUseRedactedString.new(@long_secret)
|
132
|
+
result = nil
|
133
|
+
single_use_long.expose { |val| result = val.length }
|
134
|
+
result
|
135
|
+
#=> 50
|
136
|
+
|
137
|
+
## Can manually expose the length of the value by duplicating
|
138
|
+
single_use_long = SingleUseRedactedString.new(@long_secret)
|
139
|
+
result = nil
|
140
|
+
single_use_long.expose { |val| result = val.dup.length }
|
141
|
+
result
|
142
|
+
#=> 50
|
143
|
+
|
144
|
+
## Cannot manually expose the value
|
145
|
+
single_use_special = SingleUseRedactedString.new(@special_chars)
|
146
|
+
result = nil
|
147
|
+
single_use_special.expose { |val| result = val }
|
148
|
+
result
|
149
|
+
#=> ""
|
150
|
+
|
151
|
+
## Can manually expose the value with special characters by duplicating
|
152
|
+
single_use_special = SingleUseRedactedString.new(@special_chars)
|
153
|
+
result = nil
|
154
|
+
single_use_special.expose { |val| result = val.dup }
|
155
|
+
result
|
156
|
+
#=> "tëmp!@#$%"
|
157
|
+
|
158
|
+
## Can manually expose the value with special characters by duplicating
|
159
|
+
single_use_special = SingleUseRedactedString.new(@special_chars)
|
160
|
+
result = nil
|
161
|
+
single_use_special.expose { |val| result = val.dup }
|
162
|
+
result
|
163
|
+
##=> "tëmp!@#$%"
|
164
|
+
|
165
|
+
## expose with special characters via raw method (IN TEST ONLY)
|
166
|
+
module SpecialTestonlyDirectAccess
|
167
|
+
using SingleUseRedactedStringTestHelper
|
168
|
+
single_use_special = SingleUseRedactedString.new("tëmp!@#$%")
|
169
|
+
# Use raw to access the internal value before expose clears it
|
170
|
+
single_use_special.raw
|
171
|
+
end
|
172
|
+
#=> "tëmp!@#$%"
|
173
|
+
|
174
|
+
## Cleared single-use string maintains redacted appearance
|
175
|
+
single_use_appearance = SingleUseRedactedString.new(@auth_token)
|
176
|
+
single_use_appearance.expose { |val| val } # Use and clear
|
177
|
+
single_use_appearance.to_s
|
178
|
+
#=> "[REDACTED]"
|
179
|
+
|
180
|
+
## Cleared single-use string inspect still redacted
|
181
|
+
single_use_inspect_cleared = SingleUseRedactedString.new(@otp_code)
|
182
|
+
single_use_inspect_cleared.expose { |val| val } # Use and clear
|
183
|
+
single_use_inspect_cleared.inspect
|
184
|
+
#=> "[REDACTED]"
|
185
|
+
|
186
|
+
## Object equality works same as parent (inherited behavior)
|
187
|
+
single_use1 = SingleUseRedactedString.new(@auth_token)
|
188
|
+
single_use2 = SingleUseRedactedString.new(@auth_token)
|
189
|
+
single_use1 == single_use2
|
190
|
+
#=> false
|
191
|
+
|
192
|
+
## Same object equality returns true (inherited behavior)
|
193
|
+
single_use_same = SingleUseRedactedString.new(@otp_code)
|
194
|
+
single_use_same == single_use_same
|
195
|
+
#=> true
|
196
|
+
|
197
|
+
## eql? behaves same as == (inherited behavior)
|
198
|
+
single_use_eql1 = SingleUseRedactedString.new(@encryption_key)
|
199
|
+
single_use_eql2 = SingleUseRedactedString.new(@encryption_key)
|
200
|
+
single_use_eql1.eql?(single_use_eql2)
|
201
|
+
#=> false
|
202
|
+
|
203
|
+
## Hash behavior consistent with parent (inherited behavior)
|
204
|
+
single_use_hash1 = SingleUseRedactedString.new(@auth_token)
|
205
|
+
single_use_hash2 = SingleUseRedactedString.new(@otp_code)
|
206
|
+
single_use_hash1.hash == single_use_hash2.hash
|
207
|
+
#=> true
|
208
|
+
|
209
|
+
## Hash value consistent with RedactedString class (inherited behavior)
|
210
|
+
single_use_hash = SingleUseRedactedString.new(@encryption_key)
|
211
|
+
single_use_hash.hash == RedactedString.hash
|
212
|
+
#=> true
|
213
|
+
|
214
|
+
## Cannot be used in string operations (inherited behavior)
|
215
|
+
single_use_no_concat = SingleUseRedactedString.new(@auth_token)
|
216
|
+
begin
|
217
|
+
result = single_use_no_concat + "suffix"
|
218
|
+
false # Should not reach here
|
219
|
+
rescue => e
|
220
|
+
true # Expected to raise error
|
221
|
+
end
|
222
|
+
#=> true
|
223
|
+
|
224
|
+
## Not a String subclass (inherited behavior)
|
225
|
+
single_use_type = SingleUseRedactedString.new(@otp_code)
|
226
|
+
single_use_type.is_a?(String)
|
227
|
+
#=> false
|
228
|
+
|
229
|
+
## Numeric input handling
|
230
|
+
single_use_numeric = SingleUseRedactedString.new(42)
|
231
|
+
result = nil
|
232
|
+
single_use_numeric.expose { |val| result = val.dup }
|
233
|
+
result
|
234
|
+
#=> "42"
|
235
|
+
|
236
|
+
## Symbol input handling
|
237
|
+
single_use_symbol = SingleUseRedactedString.new(:secret)
|
238
|
+
result = nil
|
239
|
+
single_use_symbol.expose { |val| result = val.dup }
|
240
|
+
result
|
241
|
+
#=> "secret"
|
242
|
+
|
243
|
+
## Nil input handling
|
244
|
+
single_use_nil = SingleUseRedactedString.new(nil)
|
245
|
+
result = nil
|
246
|
+
single_use_nil.expose { |val| result = val.dup }
|
247
|
+
result
|
248
|
+
#=> ""
|
249
|
+
|
250
|
+
## Block can return different values
|
251
|
+
single_use_return_test = SingleUseRedactedString.new("test123")
|
252
|
+
result = single_use_return_test.expose { |val| "processed: #{val.length}" }
|
253
|
+
result
|
254
|
+
#=> "processed: 7"
|
255
|
+
|
256
|
+
## clear! method works on SingleUseRedactedString (inherited behavior)
|
257
|
+
single_use_manual_clear = SingleUseRedactedString.new(@auth_token)
|
258
|
+
single_use_manual_clear.clear!
|
259
|
+
single_use_manual_clear.cleared?
|
260
|
+
#=> true
|
261
|
+
|
262
|
+
## Manual clear! prevents subsequent expose
|
263
|
+
single_use_manual_then_expose = SingleUseRedactedString.new(@otp_code)
|
264
|
+
single_use_manual_then_expose.clear!
|
265
|
+
begin
|
266
|
+
single_use_manual_then_expose.expose { |val| val }
|
267
|
+
rescue SecurityError => e
|
268
|
+
e.message
|
269
|
+
end
|
270
|
+
#=> "Value already cleared"
|
271
|
+
|
272
|
+
## freeze behavior after expose (automatic clearing freezes object)
|
273
|
+
single_use_freeze = SingleUseRedactedString.new(@encryption_key)
|
274
|
+
single_use_freeze.expose { |val| val }
|
275
|
+
single_use_freeze.frozen?
|
276
|
+
#=> true
|
277
|
+
|
278
|
+
## Working with sensitive data patterns - OTP example
|
279
|
+
otp = SingleUseRedactedString.new("123456")
|
280
|
+
verification_result = otp.expose do |code|
|
281
|
+
# Simulate OTP verification
|
282
|
+
code == "123456" ? "valid" : "invalid"
|
283
|
+
end
|
284
|
+
# OTP is now unusable
|
285
|
+
[verification_result, otp.cleared?]
|
286
|
+
#=> ["valid", true]
|
287
|
+
|
288
|
+
## Working with sensitive data patterns - temporary token example
|
289
|
+
temp_token = SingleUseRedactedString.new("temp-xyz-789")
|
290
|
+
auth_result = temp_token.expose do |token|
|
291
|
+
# Simulate authentication
|
292
|
+
{ success: true, token_length: token.length }
|
293
|
+
end
|
294
|
+
# Token is now unusable
|
295
|
+
[auth_result[:success], temp_token.cleared?]
|
296
|
+
#=> [true, true]
|
297
|
+
|
298
|
+
|
299
|
+
# TEARDOWN
|
300
|
+
|
301
|
+
# Clean up any remaining test objects
|
302
|
+
@otp_code = nil
|
303
|
+
@auth_token = nil
|
304
|
+
@encryption_key = nil
|
305
|
+
@empty_secret = nil
|
306
|
+
@long_secret = nil
|
307
|
+
@special_chars = nil
|
308
|
+
|
309
|
+
# Force garbage collection to trigger any finalizers
|
310
|
+
GC.start
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# try/features/transient_fields_core_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
class SecretService < Familia::Horreum
|
6
|
+
feature :transient_fields
|
7
|
+
|
8
|
+
field :name
|
9
|
+
field :endpoint_url
|
10
|
+
transient_field :api_key
|
11
|
+
transient_field :password
|
12
|
+
transient_field :secret_token, as: :token
|
13
|
+
end
|
14
|
+
|
15
|
+
@service = SecretService.new
|
16
|
+
@service.name = 'Test API Service'
|
17
|
+
@service.endpoint_url = 'https://api.example.com'
|
18
|
+
@service.api_key = 'sk-1234567890abcdef'
|
19
|
+
@service.password = 'super_secret_password'
|
20
|
+
@service.token = 'token-xyz789'
|
21
|
+
|
22
|
+
## Class has correct field definitions
|
23
|
+
SecretService.fields.sort
|
24
|
+
#=> [:api_key, :endpoint_url, :name, :password, :secret_token]
|
25
|
+
|
26
|
+
## Persistent fields exclude transient ones
|
27
|
+
SecretService.persistent_fields.sort
|
28
|
+
#=> [:endpoint_url, :name]
|
29
|
+
|
30
|
+
## Transient field definitions have correct category
|
31
|
+
SecretService.field_types[:api_key].category
|
32
|
+
#=> :transient
|
33
|
+
|
34
|
+
## Password field definition has correct category
|
35
|
+
SecretService.field_types[:password].category
|
36
|
+
#=> :transient
|
37
|
+
|
38
|
+
## Secret token field definition has correct category
|
39
|
+
SecretService.field_types[:secret_token].category
|
40
|
+
#=> :transient
|
41
|
+
|
42
|
+
## Regular field definition has correct category
|
43
|
+
SecretService.field_types[:name].category
|
44
|
+
#=> :field
|
45
|
+
|
46
|
+
## Transient field stores RedactedString object for api_key
|
47
|
+
@service.api_key.class
|
48
|
+
#=> RedactedString
|
49
|
+
|
50
|
+
## Transient field stores RedactedString object for password
|
51
|
+
@service.password.class
|
52
|
+
#=> RedactedString
|
53
|
+
|
54
|
+
## Transient field stores RedactedString object for token alias
|
55
|
+
@service.token.class
|
56
|
+
#=> RedactedString
|
57
|
+
|
58
|
+
## Regular field stores normal string value for name
|
59
|
+
@service.name.class
|
60
|
+
#=> String
|
61
|
+
|
62
|
+
## Regular field stores normal string value for endpoint_url
|
63
|
+
@service.endpoint_url.class
|
64
|
+
#=> String
|
65
|
+
|
66
|
+
## Transient field value is redacted in string representation
|
67
|
+
@service.api_key.to_s
|
68
|
+
#=> "[REDACTED]"
|
69
|
+
|
70
|
+
## Transient field value is redacted in inspect output
|
71
|
+
@service.password.inspect
|
72
|
+
#=> "[REDACTED]"
|
73
|
+
|
74
|
+
## Transient field can expose value securely through block
|
75
|
+
result = nil
|
76
|
+
@service.api_key.expose { |val| result = val.dup }
|
77
|
+
result
|
78
|
+
#=> "sk-1234567890abcdef"
|
79
|
+
|
80
|
+
## Transient field with custom method name exposes value correctly
|
81
|
+
result = nil
|
82
|
+
@service.token.expose { |val| result = val.dup }
|
83
|
+
result
|
84
|
+
#=> "token-xyz789"
|
85
|
+
|
86
|
+
## Setting transient field with existing RedactedString works
|
87
|
+
already_redacted = RedactedString.new('already_wrapped')
|
88
|
+
@service.password = already_redacted
|
89
|
+
@service.password.class
|
90
|
+
#=> RedactedString
|
91
|
+
|
92
|
+
## Serialization to_h only includes persistent fields
|
93
|
+
hash_result = @service.to_h
|
94
|
+
hash_result.keys.sort
|
95
|
+
#=> [:endpoint_url, :name]
|
96
|
+
|
97
|
+
## Serialization to_h excludes api_key transient field
|
98
|
+
hash_result = @service.to_h
|
99
|
+
hash_result.key?('api_key')
|
100
|
+
#=> false
|
101
|
+
|
102
|
+
## Serialization to_h excludes password transient field
|
103
|
+
hash_result = @service.to_h
|
104
|
+
hash_result.key?('password')
|
105
|
+
#=> false
|
106
|
+
|
107
|
+
## Serialization to_h excludes secret_token transient field
|
108
|
+
hash_result = @service.to_h
|
109
|
+
hash_result.key?('secret_token')
|
110
|
+
#=> false
|
111
|
+
|
112
|
+
## Serialization to_a only includes persistent field values
|
113
|
+
array_result = @service.to_a
|
114
|
+
array_result.length
|
115
|
+
#=> 2
|
116
|
+
|
117
|
+
## String interpolation with transient field shows redacted value
|
118
|
+
log_message = "Connecting to #{@service.name} with key: #{@service.api_key}"
|
119
|
+
log_message.include?('[REDACTED]')
|
120
|
+
#=> true
|
121
|
+
|
122
|
+
## String interpolation with transient field hides actual value
|
123
|
+
log_message = "Connecting to #{@service.name} with key: #{@service.api_key}"
|
124
|
+
log_message.include?('sk-1234567890abcdef')
|
125
|
+
#=> false
|
126
|
+
|
127
|
+
## Hash containing transient field shows redacted in string output
|
128
|
+
config_hash = {
|
129
|
+
service: @service.name,
|
130
|
+
key: @service.api_key,
|
131
|
+
url: @service.endpoint_url
|
132
|
+
}
|
133
|
+
config_hash.to_s.include?('[REDACTED]')
|
134
|
+
#=> true
|
135
|
+
|
136
|
+
## Hash containing transient field hides actual value in string output
|
137
|
+
config_hash = {
|
138
|
+
service: @service.name,
|
139
|
+
key: @service.api_key,
|
140
|
+
url: @service.endpoint_url
|
141
|
+
}
|
142
|
+
config_hash.to_s.include?('sk-1234567890abcdef')
|
143
|
+
#=> false
|
144
|
+
|
145
|
+
## Exception messages with transient fields are safe
|
146
|
+
begin
|
147
|
+
raise StandardError, "Failed to authenticate with key: #{@service.api_key}"
|
148
|
+
rescue StandardError => e
|
149
|
+
e.message.include?('[REDACTED]')
|
150
|
+
end
|
151
|
+
#=> true
|
152
|
+
|
153
|
+
## Multiple transient field assignment creates RedactedString instances
|
154
|
+
new_service = SecretService.new
|
155
|
+
new_service.name = 'Another Service'
|
156
|
+
new_service.api_key = 'new-api-key-123'
|
157
|
+
new_service.password = 'new-password-456'
|
158
|
+
new_service.token = 'new-token-789'
|
159
|
+
[new_service.api_key, new_service.password, new_service.token].all? { |f| f.is_a?(RedactedString) }
|
160
|
+
#=> true
|
161
|
+
|
162
|
+
## Transient field can be set to nil value
|
163
|
+
new_service = SecretService.new
|
164
|
+
new_service.api_key = nil
|
165
|
+
new_service.api_key
|
166
|
+
#=> nil
|
167
|
+
|
168
|
+
## Persistent field definitions are correctly identified
|
169
|
+
SecretService.field_types.values.select(&:persistent?).map(&:name).sort
|
170
|
+
#=> [:endpoint_url, :name]
|
171
|
+
|
172
|
+
## Transient field definitions are correctly identified
|
173
|
+
transient_fields = SecretService.field_types.values.reject(&:persistent?).map(&:name).sort
|
174
|
+
transient_fields
|
175
|
+
#=> [:api_key, :password, :secret_token]
|
176
|
+
|
177
|
+
# Clean up test objects
|
178
|
+
@service = nil
|
179
|
+
|
180
|
+
# Force garbage collection to trigger any finalizers
|
181
|
+
GC.start
|