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.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +11 -8
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +19 -3
  7. data/README.md +36 -157
  8. data/docs/overview.md +359 -0
  9. data/docs/wiki/API-Reference.md +347 -0
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +600 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +106 -0
  17. data/docs/wiki/Implementation-Guide.md +276 -0
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/RelatableObjects-Guide.md +563 -0
  20. data/docs/wiki/Security-Model.md +183 -0
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/lib/familia/base.rb +18 -27
  23. data/lib/familia/connection.rb +6 -5
  24. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  25. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  26. data/lib/familia/data_type/types/counter.rb +38 -0
  27. data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
  28. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  31. data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
  32. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  33. data/lib/familia/{datatype.rb → data_type.rb} +12 -14
  34. data/lib/familia/encryption/encrypted_data.rb +137 -0
  35. data/lib/familia/encryption/manager.rb +119 -0
  36. data/lib/familia/encryption/provider.rb +49 -0
  37. data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
  38. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  39. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
  40. data/lib/familia/encryption/registry.rb +50 -0
  41. data/lib/familia/encryption.rb +178 -0
  42. data/lib/familia/encryption_request_cache.rb +68 -0
  43. data/lib/familia/errors.rb +17 -3
  44. data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
  45. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
  46. data/lib/familia/features/encrypted_fields.rb +28 -0
  47. data/lib/familia/features/expiration.rb +107 -77
  48. data/lib/familia/features/quantization.rb +5 -9
  49. data/lib/familia/features/relatable_objects.rb +2 -4
  50. data/lib/familia/features/safe_dump.rb +14 -17
  51. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  52. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  53. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  54. data/lib/familia/features/transient_fields.rb +47 -0
  55. data/lib/familia/features.rb +40 -24
  56. data/lib/familia/field_type.rb +273 -0
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
  58. data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
  63. data/lib/familia/horreum/subclass/definition.rb +469 -0
  64. data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +30 -22
  67. data/lib/familia/logging.rb +14 -14
  68. data/lib/familia/settings.rb +39 -3
  69. data/lib/familia/utils.rb +45 -0
  70. data/lib/familia/version.rb +1 -1
  71. data/lib/familia.rb +3 -2
  72. data/try/core/base_enhancements_try.rb +115 -0
  73. data/try/core/connection_try.rb +0 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -5
  77. data/try/core/familia_extended_try.rb +3 -4
  78. data/try/core/familia_try.rb +1 -2
  79. data/try/core/persistence_operations_try.rb +297 -0
  80. data/try/core/pools_try.rb +2 -2
  81. data/try/core/secure_identifier_try.rb +0 -1
  82. data/try/core/settings_try.rb +0 -1
  83. data/try/core/utils_try.rb +0 -1
  84. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  85. data/try/data_types/counter_try.rb +93 -0
  86. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  87. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  88. data/try/{datatypes → data_types}/list_try.rb +1 -2
  89. data/try/data_types/lock_try.rb +133 -0
  90. data/try/{datatypes → data_types}/set_try.rb +1 -2
  91. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  92. data/try/{datatypes → data_types}/string_try.rb +1 -2
  93. data/try/debugging/README.md +32 -0
  94. data/try/debugging/cache_behavior_tracer.rb +91 -0
  95. data/try/debugging/debug_aad_process.rb +82 -0
  96. data/try/debugging/debug_concealed_internal.rb +59 -0
  97. data/try/debugging/debug_concealed_reveal.rb +61 -0
  98. data/try/debugging/debug_context_aad.rb +68 -0
  99. data/try/debugging/debug_context_simple.rb +80 -0
  100. data/try/debugging/debug_cross_context.rb +62 -0
  101. data/try/debugging/debug_database_load.rb +64 -0
  102. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  103. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  104. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  105. data/try/debugging/debug_field_decrypt.rb +74 -0
  106. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  107. data/try/debugging/debug_load_path.rb +66 -0
  108. data/try/debugging/debug_method_definition.rb +46 -0
  109. data/try/debugging/debug_method_resolution.rb +41 -0
  110. data/try/debugging/debug_minimal.rb +24 -0
  111. data/try/debugging/debug_provider.rb +68 -0
  112. data/try/debugging/debug_secure_behavior.rb +73 -0
  113. data/try/debugging/debug_string_class.rb +46 -0
  114. data/try/debugging/debug_test.rb +46 -0
  115. data/try/debugging/debug_test_design.rb +80 -0
  116. data/try/debugging/encryption_method_tracer.rb +138 -0
  117. data/try/debugging/provider_diagnostics.rb +110 -0
  118. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  119. data/try/edge_cases/json_serialization_try.rb +0 -1
  120. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  121. data/try/encryption/config_persistence_try.rb +192 -0
  122. data/try/encryption/encryption_core_try.rb +328 -0
  123. data/try/encryption/instance_variable_scope_try.rb +31 -0
  124. data/try/encryption/module_loading_try.rb +28 -0
  125. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  126. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  127. data/try/encryption/roundtrip_validation_try.rb +28 -0
  128. data/try/encryption/secure_memory_handling_try.rb +125 -0
  129. data/try/features/encrypted_fields_core_try.rb +125 -0
  130. data/try/features/encrypted_fields_integration_try.rb +216 -0
  131. data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
  132. data/try/features/encrypted_fields_security_try.rb +377 -0
  133. data/try/features/encryption_fields/aad_protection_try.rb +138 -0
  134. data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
  135. data/try/features/encryption_fields/context_isolation_try.rb +141 -0
  136. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  137. data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
  138. data/try/features/encryption_fields/fresh_key_try.rb +168 -0
  139. data/try/features/encryption_fields/key_rotation_try.rb +123 -0
  140. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  141. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  142. data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
  143. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  144. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  145. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  146. data/try/features/expiration_try.rb +0 -1
  147. data/try/features/feature_dependencies_try.rb +159 -0
  148. data/try/features/quantization_try.rb +0 -1
  149. data/try/features/real_feature_integration_try.rb +148 -0
  150. data/try/features/relatable_objects_try.rb +0 -1
  151. data/try/features/safe_dump_advanced_try.rb +0 -1
  152. data/try/features/safe_dump_try.rb +0 -1
  153. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  154. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  155. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  156. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  157. data/try/features/transient_fields_core_try.rb +181 -0
  158. data/try/features/transient_fields_integration_try.rb +260 -0
  159. data/try/helpers/test_helpers.rb +67 -0
  160. data/try/horreum/base_try.rb +157 -3
  161. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  162. data/try/horreum/field_categories_try.rb +118 -0
  163. data/try/horreum/field_definition_try.rb +96 -0
  164. data/try/horreum/initialization_try.rb +1 -2
  165. data/try/horreum/relations_try.rb +1 -2
  166. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  167. data/try/horreum/serialization_try.rb +41 -7
  168. data/try/memory/memory_basic_test.rb +73 -0
  169. data/try/memory/memory_detailed_test.rb +121 -0
  170. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  171. data/try/memory/memory_search_for_string.rb +83 -0
  172. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  173. data/try/models/customer_safe_dump_try.rb +1 -2
  174. data/try/models/customer_try.rb +1 -2
  175. data/try/models/datatype_base_try.rb +1 -2
  176. data/try/models/familia_object_try.rb +0 -1
  177. metadata +131 -23
  178. data/lib/familia/horreum/serialization.rb +0 -445
@@ -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.trace :included, base, self, caller(1..1) if Familia.debug?
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.trace :INCLUDED, base, self, caller(1..1) if Familia.debug?
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.trace :feature_load, base, self, caller(1..1) if Familia.debug?
45
+ base.extend ClassMethods
46
+ end
47
+
48
+ module ClassMethods
49
+ def test_feature_c_method
50
+ "feature_c_with_deps_active"
51
+ end
52
+ end
53
+
54
+ def test_feature_c_instance
55
+ "instance_feature_c_with_deps"
56
+ end
57
+ end
58
+
59
+ # Register test features manually
60
+ Familia::Base.add_feature TestFeatureA, :test_feature_a
61
+ Familia::Base.add_feature TestFeatureB, :test_feature_b
62
+ Familia::Base.add_feature TestFeatureCWithDeps, :test_feature_c, depends_on: [:test_feature_a, :test_feature_b]
63
+
64
+ ## Feature definitions are created correctly
65
+ Familia::Base.feature_definitions.key?(:test_feature_a)
66
+ #=> true
67
+
68
+ ## Feature definitions store dependencies correctly
69
+ Familia::Base.feature_definitions[:test_feature_c].depends_on
70
+ #=> [:test_feature_a, :test_feature_b]
71
+
72
+ ## Features without dependencies have empty depends_on array
73
+ Familia::Base.feature_definitions[:test_feature_a].depends_on
74
+ #=> []
75
+
76
+ ## Feature definitions store name correctly
77
+ Familia::Base.feature_definitions[:test_feature_c].name
78
+ #=> :test_feature_c
79
+
80
+ ## Successfully enable feature without dependencies
81
+ class NoDepsTest < Familia::Horreum
82
+ identifier_field :id
83
+ field :id
84
+ feature :test_feature_a
85
+ end
86
+ @nodeps = NoDepsTest.new(id: 'test1')
87
+ @nodeps.test_feature_a_instance
88
+ #=> "instance_feature_a"
89
+
90
+ ## Successfully enable multiple features in correct order
91
+ class MultiFeatureTest < Familia::Horreum
92
+ identifier_field :id
93
+ field :id
94
+ feature :test_feature_a
95
+ feature :test_feature_b
96
+ feature :test_feature_c
97
+ end
98
+ @multitest = MultiFeatureTest.new(id: 'test2')
99
+ @multitest.test_feature_c_instance
100
+ #=> "instance_feature_c_with_deps"
101
+
102
+ ## Class methods from dependent features are available
103
+ MultiFeatureTest.test_feature_c_method
104
+ #=> "feature_c_with_deps_active"
105
+
106
+ ## All prerequisite features are available in features_enabled
107
+ MultiFeatureTest.features_enabled.include?(:test_feature_a)
108
+ #=> true
109
+
110
+ ## All prerequisite features are available
111
+ MultiFeatureTest.features_enabled.include?(:test_feature_b)
112
+ #=> true
113
+
114
+ ## Dependent feature is available
115
+ MultiFeatureTest.features_enabled.include?(:test_feature_c)
116
+ #=> true
117
+
118
+ ## Feature dependency validation fails when dependencies missing
119
+ class MissingDepsTest < Familia::Horreum
120
+ identifier_field :id
121
+ field :id
122
+ feature :test_feature_c # Missing dependencies should cause error
123
+ end
124
+ #=!> Familia::Problem
125
+
126
+ ## Partial dependencies cause validation failure
127
+ class PartialDepsTest < Familia::Horreum
128
+ identifier_field :id
129
+ field :id
130
+ feature :test_feature_a # Only one of two required dependencies
131
+ feature :test_feature_c
132
+ end
133
+ #=!> Familia::Problem
134
+
135
+ ## Invalid feature name raises appropriate error
136
+ class InvalidFeatureTest < Familia::Horreum
137
+ identifier_field :id
138
+ field :id
139
+ feature :nonexistent_feature
140
+ end
141
+ #=!> Familia::Problem
142
+
143
+ ## Duplicate feature inclusion gives warning but continues
144
+ class DuplicateFeatureTest < Familia::Horreum
145
+ identifier_field :id
146
+ field :id
147
+ feature :test_feature_a
148
+ feature :test_feature_a # Duplicate should warn
149
+ end
150
+ @duplicate_test = DuplicateFeatureTest.new(id: 'dup1')
151
+ @duplicate_test.test_feature_a_instance
152
+ #=> "instance_feature_a"
153
+
154
+ @nodeps.destroy! rescue nil
155
+ @multitest.destroy! rescue nil
156
+ @duplicate_test.destroy! rescue nil
157
+ @nodeps = nil
158
+ @multitest = nil
159
+ @duplicate_test = nil
@@ -1,6 +1,5 @@
1
1
  # try/features/quantization_try.rb
2
2
 
3
- require_relative '../../lib/familia'
4
3
  require_relative '../helpers/test_helpers'
5
4
 
6
5
  Familia.debug = false
@@ -0,0 +1,148 @@
1
+ # try/features/real_feature_integration_try.rb
2
+
3
+ require_relative '../helpers/test_helpers'
4
+
5
+ Familia.debug = false
6
+
7
+ # Real feature integration: expiration feature works with new system
8
+ class ExpirationIntegrationTest < Familia::Horreum
9
+ identifier_field :id
10
+ field :id
11
+ field :name
12
+ feature :expiration
13
+ end
14
+
15
+ # Safe dump feature integration with field categories
16
+ class SafeDumpCategoryTest < Familia::Horreum
17
+ identifier_field :id
18
+ field :id
19
+ field :public_name, category: :persistent
20
+ field :email, category: :encrypted
21
+ field :tryouts_cache_data, category: :transient
22
+
23
+ feature :safe_dump
24
+
25
+ @safe_dump_fields = [
26
+ :id,
27
+ :public_name,
28
+ :email
29
+ ]
30
+ end
31
+
32
+ # Combined features work together
33
+ class CombinedFeaturesTest < Familia::Horreum
34
+ identifier_field :id
35
+ field :id
36
+ field :name, category: :persistent
37
+ field :temp_data, category: :transient
38
+
39
+ feature :expiration
40
+ feature :safe_dump
41
+
42
+ @safe_dump_fields = [:id, :name]
43
+ end
44
+
45
+ # Test that individual features can be queried
46
+ class QueryFeaturesTest < Familia::Horreum
47
+ identifier_field :id
48
+ field :id
49
+ feature :expiration
50
+ end
51
+
52
+ # Empty features list for class without features
53
+ class NoFeaturesTest < Familia::Horreum
54
+ identifier_field :id
55
+ field :id
56
+ end
57
+
58
+ # Error handling for duplicate feature includes
59
+ class DuplicateFeatureHandling < Familia::Horreum
60
+ identifier_field :id
61
+ field :id
62
+ feature :expiration
63
+ # This should generate a warning but not error
64
+ feature :expiration
65
+ end
66
+
67
+ @expiration_test = ExpirationIntegrationTest.new(id: 'exp_test_1', name: 'Test')
68
+
69
+ @safedump_test = SafeDumpCategoryTest.new(
70
+ id: 'safe_test_1',
71
+ public_name: 'Public Name',
72
+ email: 'test@example.com',
73
+ tryouts_cache_data: 'temporary'
74
+ )
75
+
76
+ @combined_test = CombinedFeaturesTest.new(id: 'combined_1', name: 'Combined', temp_data: 'temp')
77
+
78
+ ## Expiration feature is properly registered
79
+ Familia::Base.features_available.key?(:expiration)
80
+ #=> true
81
+
82
+ ## Feature enabled correctly
83
+ ExpirationIntegrationTest.features_enabled.include?(:expiration)
84
+ #=> true
85
+
86
+ ## Expiration methods are available
87
+ @expiration_test.respond_to?(:update_expiration)
88
+ #=> true
89
+
90
+ ## Class methods from expiration feature work
91
+ ExpirationIntegrationTest.respond_to?(:default_expiration)
92
+ #=> true
93
+
94
+ ## Safe dump feature loaded correctly
95
+ SafeDumpCategoryTest.features_enabled.include?(:safe_dump)
96
+ #=> true
97
+
98
+ ## Safe dump works with field categories
99
+ @safedump_result = @safedump_test.safe_dump
100
+ @safedump_result.keys.sort
101
+ #=> [:email, :id, :public_name]
102
+
103
+ ## Safe dump respects safe_dump_fields configuration
104
+ @safedump_result.key?(:tryouts_cache_data)
105
+ #=> false
106
+
107
+ ## Both features are enabled
108
+ CombinedFeaturesTest.features_enabled.include?(:expiration)
109
+ #=> true
110
+
111
+ ## Safe dump feature also enabled
112
+ CombinedFeaturesTest.features_enabled.include?(:safe_dump)
113
+ #=> true
114
+
115
+ ## Combined functionality works correctly
116
+ @combined_test.safe_dump
117
+ #=> { id: "combined_1", name: "Combined" }
118
+
119
+ ## Expiration functionality still available
120
+ @combined_test.respond_to?(:update_expiration)
121
+ #=> true
122
+
123
+ ## Test that feature() method returns current features when called with no args
124
+ CombinedFeaturesTest.feature
125
+ #=> [:expiration, :safe_dump]
126
+
127
+ ## Test that features_enabled() method returns the same results as feature() method
128
+ CombinedFeaturesTest.feature
129
+ #=> [:expiration, :safe_dump]
130
+
131
+ ## Features list is accessible
132
+ QueryFeaturesTest.feature
133
+ #=> [:expiration]
134
+
135
+ ## No features returns empty array
136
+ NoFeaturesTest.feature
137
+ #=> []
138
+
139
+ ## Duplicate features handled gracefully
140
+ DuplicateFeatureHandling.features_enabled
141
+ #=> [:expiration]
142
+
143
+ @expiration_test.destroy! rescue nil
144
+ @safedump_test.destroy! rescue nil
145
+ @combined_test.destroy! rescue nil
146
+ @expiration_test = nil
147
+ @safedump_test = nil
148
+ @combined_test = nil
@@ -2,7 +2,6 @@
2
2
 
3
3
  # Test RelatableObject feature functionality
4
4
 
5
- require_relative '../../lib/familia'
6
5
  require_relative '../helpers/test_helpers'
7
6
 
8
7
  Familia.debug = false
@@ -2,7 +2,6 @@
2
2
 
3
3
  # These tryouts test the safe dumping functionality.
4
4
 
5
- require_relative '../../lib/familia'
6
5
  require_relative '../helpers/test_helpers'
7
6
 
8
7
  ## By default Familia::Base has no safe_dump_fields method
@@ -1,6 +1,5 @@
1
1
  # try/features/safe_dump_try.rb
2
2
 
3
- require_relative '../../lib/familia'
4
3
  require_relative '../helpers/test_helpers'
5
4
 
6
5
  Familia.debug = false
@@ -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