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,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
|
@@ -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
|
@@ -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
|