familia 2.0.0.pre3 → 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 -229
- 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/class_methods_try.rb +27 -36
- 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,176 @@
|
|
1
|
+
# try/horreum/enhanced_conflict_handling_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
Familia.debug = false
|
6
|
+
|
7
|
+
## Valid strategies are defined correctly
|
8
|
+
Familia::VALID_STRATEGIES.include?(:raise)
|
9
|
+
#=> true
|
10
|
+
|
11
|
+
## Valid strategies include all expected options
|
12
|
+
Familia::VALID_STRATEGIES
|
13
|
+
#=> [:raise, :skip, :warn, :overwrite]
|
14
|
+
|
15
|
+
## Overwrite strategy removes existing method and defines new one
|
16
|
+
class OverwriteStrategyTest < Familia::Horreum
|
17
|
+
identifier_field :id
|
18
|
+
field :id
|
19
|
+
|
20
|
+
def conflicting_method
|
21
|
+
"original_method"
|
22
|
+
end
|
23
|
+
|
24
|
+
field :conflicting_method, on_conflict: :overwrite
|
25
|
+
end
|
26
|
+
@overwrite_test = OverwriteStrategyTest.new(id: 'overwrite1')
|
27
|
+
@overwrite_test.conflicting_method = "new_value"
|
28
|
+
@overwrite_test.conflicting_method
|
29
|
+
#=> "new_value"
|
30
|
+
|
31
|
+
## Overwrite strategy works with fast methods too
|
32
|
+
@overwrite_test.save
|
33
|
+
@overwrite_test.conflicting_method! "fast_value"
|
34
|
+
#=> true
|
35
|
+
|
36
|
+
## Invalid conflict strategy raises error during field definition
|
37
|
+
class InvalidStrategyTest < Familia::Horreum
|
38
|
+
identifier_field :id
|
39
|
+
field :id
|
40
|
+
field :test_field, on_conflict: :invalid_strategy
|
41
|
+
end
|
42
|
+
#=!> ArgumentError
|
43
|
+
|
44
|
+
## Method conflict detection works with instance methods
|
45
|
+
class ConflictDetectionTest < Familia::Horreum
|
46
|
+
identifier_field :id
|
47
|
+
field :id
|
48
|
+
|
49
|
+
def existing_method
|
50
|
+
"exists"
|
51
|
+
end
|
52
|
+
|
53
|
+
field :existing_method, on_conflict: :raise
|
54
|
+
end
|
55
|
+
#=!> ArgumentError
|
56
|
+
|
57
|
+
## Conflict detection provides helpful error message
|
58
|
+
begin
|
59
|
+
class ConflictMessageTest < Familia::Horreum
|
60
|
+
identifier_field :id
|
61
|
+
field :id
|
62
|
+
|
63
|
+
def another_method
|
64
|
+
"exists"
|
65
|
+
end
|
66
|
+
|
67
|
+
field :another_method, on_conflict: :raise
|
68
|
+
end
|
69
|
+
rescue ArgumentError => e
|
70
|
+
e.message.include?("another_method")
|
71
|
+
end
|
72
|
+
#=> true
|
73
|
+
|
74
|
+
## Method location information in error message when possible
|
75
|
+
|
76
|
+
class LocationInfoTest < Familia::Horreum
|
77
|
+
identifier_field :id
|
78
|
+
field :id
|
79
|
+
|
80
|
+
def location_test_method
|
81
|
+
"exists"
|
82
|
+
end
|
83
|
+
|
84
|
+
field :location_test_method, on_conflict: :raise
|
85
|
+
end
|
86
|
+
#=!> ArgumentError
|
87
|
+
#==> error.message.include?("already defined")
|
88
|
+
|
89
|
+
## Skip strategy silently ignores conflicts
|
90
|
+
class SkipStrategyTest < Familia::Horreum
|
91
|
+
identifier_field :id
|
92
|
+
field :id
|
93
|
+
|
94
|
+
def skip_method
|
95
|
+
"original"
|
96
|
+
end
|
97
|
+
|
98
|
+
field :skip_method, on_conflict: :skip
|
99
|
+
end
|
100
|
+
@skip_test = SkipStrategyTest.new(id: 'skip1')
|
101
|
+
@skip_test.skip_method
|
102
|
+
#=> "original"
|
103
|
+
|
104
|
+
## Skip strategy doesn't create accessor methods when method exists
|
105
|
+
@skip_test.respond_to?(:skip_method=)
|
106
|
+
#=> false
|
107
|
+
|
108
|
+
## Warn strategy shows warning but continues with definition
|
109
|
+
class WarnStrategyTest < Familia::Horreum
|
110
|
+
identifier_field :id
|
111
|
+
field :id
|
112
|
+
|
113
|
+
def warn_method
|
114
|
+
"original"
|
115
|
+
end
|
116
|
+
|
117
|
+
field :warn_method, on_conflict: :warn
|
118
|
+
end
|
119
|
+
#=2> /WARNING/
|
120
|
+
@warn_test = WarnStrategyTest.new(id: 'warn1')
|
121
|
+
@warn_test.warn_method = "new_value"
|
122
|
+
@warn_test.warn_method
|
123
|
+
#=> "new_value"
|
124
|
+
|
125
|
+
## Fast method names must end with exclamation mark
|
126
|
+
class InvalidFastMethodTest < Familia::Horreum
|
127
|
+
identifier_field :id
|
128
|
+
field :id
|
129
|
+
field :test_field, fast_method: :invalid_name
|
130
|
+
end
|
131
|
+
#=!> ArgumentError
|
132
|
+
|
133
|
+
## Fast method validation works with custom names
|
134
|
+
class ValidFastMethodTest < Familia::Horreum
|
135
|
+
identifier_field :id
|
136
|
+
field :id
|
137
|
+
field :score, fast_method: :update_score_now!
|
138
|
+
end
|
139
|
+
@valid_fast = ValidFastMethodTest.new(id: 'valid1')
|
140
|
+
@valid_fast.respond_to?(:update_score_now!)
|
141
|
+
#=> true
|
142
|
+
|
143
|
+
## Method added hook detects conflicts after field definition
|
144
|
+
class MethodAddedHookTest < Familia::Horreum
|
145
|
+
identifier_field :id
|
146
|
+
field :id
|
147
|
+
field :hook_test, on_conflict: :warn
|
148
|
+
|
149
|
+
def hook_test
|
150
|
+
"redefined_after"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
#=2> /WARNING/
|
154
|
+
#=2> /hook_test/
|
155
|
+
#=2> /redefined after field definition/
|
156
|
+
|
157
|
+
## Method added hook works with raise strategy too
|
158
|
+
class MethodAddedRaiseTest < Familia::Horreum
|
159
|
+
identifier_field :id
|
160
|
+
field :id
|
161
|
+
field :raise_hook_test, on_conflict: :raise
|
162
|
+
|
163
|
+
def raise_hook_test
|
164
|
+
"redefined"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
#=!> ArgumentError
|
168
|
+
|
169
|
+
@overwrite_test.destroy! rescue nil
|
170
|
+
@skip_test.destroy! rescue nil
|
171
|
+
@warn_test.destroy! rescue nil
|
172
|
+
@valid_fast.destroy! rescue nil
|
173
|
+
@overwrite_test = nil
|
174
|
+
@skip_test = nil
|
175
|
+
@warn_test = nil
|
176
|
+
@valid_fast = nil
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# try/horreum/field_categories_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
Familia.debug = false
|
6
|
+
|
7
|
+
# Define test class with various field categories
|
8
|
+
class FieldCategoryTest < Familia::Horreum
|
9
|
+
identifier_field :id
|
10
|
+
field :id
|
11
|
+
field :name # default category (:field)
|
12
|
+
field :email, category: :encrypted # encrypted category
|
13
|
+
field :tryouts_cache_data, category: :transient # transient category
|
14
|
+
field :description, category: :persistent # explicit persistent category
|
15
|
+
field :settings, category: nil # nil category (defaults to :field)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Test class with multiple transient fields
|
19
|
+
class MultiTransientTest < Familia::Horreum
|
20
|
+
identifier_field :id
|
21
|
+
field :id
|
22
|
+
field :permanent_data
|
23
|
+
field :temp1, category: :transient
|
24
|
+
field :temp2, category: :transient
|
25
|
+
field :temp3, category: :transient
|
26
|
+
end
|
27
|
+
|
28
|
+
# Field categories work with field aliasing
|
29
|
+
class AliasedCategoryTest < Familia::Horreum
|
30
|
+
identifier_field :id
|
31
|
+
field :id
|
32
|
+
field :internal_temp, as: :temp, category: :transient
|
33
|
+
field :internal_perm, as: :perm, category: :persistent
|
34
|
+
end
|
35
|
+
|
36
|
+
# Test edge case with all transient fields
|
37
|
+
class AllTransientTest < Familia::Horreum
|
38
|
+
identifier_field :id
|
39
|
+
field :id
|
40
|
+
field :temp1, category: :transient
|
41
|
+
field :temp2, category: :transient
|
42
|
+
end
|
43
|
+
|
44
|
+
## Field types are stored correctly
|
45
|
+
@test_obj = FieldCategoryTest.new(id: 'test123')
|
46
|
+
FieldCategoryTest.field_types.size
|
47
|
+
#=> 6
|
48
|
+
|
49
|
+
## Default category field has correct category
|
50
|
+
FieldCategoryTest.field_types[:name].category
|
51
|
+
#=> :field
|
52
|
+
|
53
|
+
## Encrypted category field has correct category
|
54
|
+
FieldCategoryTest.field_types[:email].category
|
55
|
+
#=> :encrypted
|
56
|
+
|
57
|
+
## Transient category field has correct category
|
58
|
+
FieldCategoryTest.field_types[:tryouts_cache_data].category
|
59
|
+
#=> :transient
|
60
|
+
|
61
|
+
## Explicit persistent category field has correct category
|
62
|
+
FieldCategoryTest.field_types[:description].category
|
63
|
+
#=> :persistent
|
64
|
+
|
65
|
+
## Nil category field defaults to :field
|
66
|
+
FieldCategoryTest.field_types[:settings].category
|
67
|
+
#=> :field
|
68
|
+
|
69
|
+
## persistent_fields excludes transient fields
|
70
|
+
FieldCategoryTest.persistent_fields
|
71
|
+
#=> [:id, :name, :email, :description, :settings]
|
72
|
+
|
73
|
+
## persistent_fields includes encrypted and persistent fields
|
74
|
+
FieldCategoryTest.persistent_fields.include?(:email)
|
75
|
+
#=> true
|
76
|
+
|
77
|
+
## persistent_fields includes default category fields
|
78
|
+
FieldCategoryTest.persistent_fields.include?(:name)
|
79
|
+
#=> true
|
80
|
+
|
81
|
+
## persistent_fields excludes transient fields
|
82
|
+
FieldCategoryTest.persistent_fields.include?(:tryouts_cache_data)
|
83
|
+
#=> false
|
84
|
+
|
85
|
+
## Field definitions map provides backward compatibility
|
86
|
+
FieldCategoryTest.field_method_map[:name]
|
87
|
+
#=> :name
|
88
|
+
|
89
|
+
## Field definitions map works for all fields
|
90
|
+
FieldCategoryTest.field_method_map[:email]
|
91
|
+
#=> :email
|
92
|
+
|
93
|
+
## Multiple transient fields are handled correctly
|
94
|
+
MultiTransientTest.persistent_fields
|
95
|
+
#=> [:id, :permanent_data]
|
96
|
+
|
97
|
+
## Aliased transient field is excluded from persistent_fields
|
98
|
+
AliasedCategoryTest.persistent_fields.include?(:internal_temp)
|
99
|
+
#=> false
|
100
|
+
|
101
|
+
## Aliased persistent field is included in persistent_fields
|
102
|
+
AliasedCategoryTest.persistent_fields.include?(:internal_perm)
|
103
|
+
#=> true
|
104
|
+
|
105
|
+
## Field type stores original field name, not alias
|
106
|
+
AliasedCategoryTest.field_types[:internal_temp].name
|
107
|
+
#=> :internal_temp
|
108
|
+
|
109
|
+
## Field type stores alias as method name
|
110
|
+
AliasedCategoryTest.field_types[:internal_temp].method_name
|
111
|
+
#=> :temp
|
112
|
+
|
113
|
+
## persistent_fields with mostly transient fields
|
114
|
+
AllTransientTest.persistent_fields
|
115
|
+
#=> [:id]
|
116
|
+
|
117
|
+
@test_obj.destroy! rescue nil
|
118
|
+
@test_obj = nil
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# try/horreum/field_definition_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
Familia.debug = false
|
6
|
+
|
7
|
+
# Create a custom field type for testing with category support
|
8
|
+
class TestFieldType < Familia::FieldType
|
9
|
+
def initialize(name, category: :field, **kwargs)
|
10
|
+
super(name, **kwargs)
|
11
|
+
@category = category
|
12
|
+
end
|
13
|
+
|
14
|
+
def category
|
15
|
+
@category || :field
|
16
|
+
end
|
17
|
+
|
18
|
+
def persistent?
|
19
|
+
category != :transient
|
20
|
+
end
|
21
|
+
|
22
|
+
def transient?
|
23
|
+
!persistent?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Setup a test field type (replacing the old FieldDefinition)
|
28
|
+
@field_type = TestFieldType.new(
|
29
|
+
:email,
|
30
|
+
as: :email,
|
31
|
+
fast_method: :email!,
|
32
|
+
on_conflict: :raise,
|
33
|
+
category: :encrypted
|
34
|
+
)
|
35
|
+
|
36
|
+
## FieldType holds field name correctly
|
37
|
+
@field_type.name
|
38
|
+
#=> :email
|
39
|
+
|
40
|
+
## FieldType holds method name correctly
|
41
|
+
@field_type.method_name
|
42
|
+
#=> :email
|
43
|
+
|
44
|
+
## FieldType holds fast method name correctly
|
45
|
+
@field_type.fast_method_name
|
46
|
+
#=> :email!
|
47
|
+
|
48
|
+
## FieldType holds conflict strategy correctly
|
49
|
+
@field_type.on_conflict
|
50
|
+
#=> :raise
|
51
|
+
|
52
|
+
## FieldType holds category correctly
|
53
|
+
@field_type.category
|
54
|
+
#=> :encrypted
|
55
|
+
|
56
|
+
## FieldType returns generated methods list
|
57
|
+
@field_type.generated_methods
|
58
|
+
#=> [:email, :email!]
|
59
|
+
|
60
|
+
## FieldType with nil category defaults to :field
|
61
|
+
@basic_field = TestFieldType.new(
|
62
|
+
:name,
|
63
|
+
as: :name,
|
64
|
+
fast_method: :name!,
|
65
|
+
on_conflict: :skip,
|
66
|
+
category: nil
|
67
|
+
)
|
68
|
+
@basic_field.category
|
69
|
+
#=> :field
|
70
|
+
|
71
|
+
## FieldType persistent? returns true for non-transient fields
|
72
|
+
@field_type.persistent?
|
73
|
+
#=> true
|
74
|
+
|
75
|
+
## FieldType persistent? returns false for transient fields
|
76
|
+
@transient_field = TestFieldType.new(
|
77
|
+
:temp_data,
|
78
|
+
as: :temp_data,
|
79
|
+
fast_method: :temp_data!,
|
80
|
+
on_conflict: :raise,
|
81
|
+
category: :transient
|
82
|
+
)
|
83
|
+
@transient_field.persistent?
|
84
|
+
#=> false
|
85
|
+
|
86
|
+
## FieldType to_s includes all attributes
|
87
|
+
@field_type.to_s
|
88
|
+
#=~>/#<.*TestFieldType name=email method_name=email fast_method_name=email! on_conflict=raise category=encrypted>/
|
89
|
+
|
90
|
+
## FieldType inspect is same as to_s
|
91
|
+
@field_type.inspect
|
92
|
+
#=~>/#<.*TestFieldType name=email method_name=email fast_method_name=email! on_conflict=raise category=encrypted>/
|
93
|
+
|
94
|
+
@field_type = nil
|
95
|
+
@basic_field = nil
|
96
|
+
@transient_field = nil
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# try/horreum/serialization_persistent_fields_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
Familia.debug = false
|
6
|
+
|
7
|
+
# Test class with mixed field categories for serialization
|
8
|
+
class SerializationCategoryTest < Familia::Horreum
|
9
|
+
identifier_field :id
|
10
|
+
field :id
|
11
|
+
field :name # persistent by default
|
12
|
+
field :email, category: :encrypted # persistent, encrypted category
|
13
|
+
field :tryouts_cache_data, category: :transient # should be excluded from serialization
|
14
|
+
field :description, category: :persistent # explicitly persistent
|
15
|
+
field :temp_settings, category: :transient # should be excluded
|
16
|
+
field :metadata, category: :persistent # explicitly persistent
|
17
|
+
end
|
18
|
+
|
19
|
+
# Class with all transient fields
|
20
|
+
class AllTransientSerializationTest < Familia::Horreum
|
21
|
+
identifier_field :id
|
22
|
+
field :id
|
23
|
+
field :temp1, category: :transient
|
24
|
+
field :temp2, category: :transient
|
25
|
+
end
|
26
|
+
|
27
|
+
# Mixed categories with aliased fields
|
28
|
+
class AliasedSerializationTest < Familia::Horreum
|
29
|
+
identifier_field :id
|
30
|
+
field :id
|
31
|
+
field :internal_name, as: :display_name, category: :persistent
|
32
|
+
field :temp_cache, as: :cache, category: :transient
|
33
|
+
field :user_data, as: :data, category: :encrypted
|
34
|
+
end
|
35
|
+
|
36
|
+
# Setup test instance with all field types
|
37
|
+
@serialization_test = SerializationCategoryTest.new(
|
38
|
+
id: 'serialize_test_1',
|
39
|
+
name: 'Test User',
|
40
|
+
email: 'test@example.com',
|
41
|
+
tryouts_cache_data: 'temporary_cache_value',
|
42
|
+
description: 'A test user description',
|
43
|
+
temp_settings: { theme: 'dark', cache: true },
|
44
|
+
metadata: { version: 1, last_login: '2025-01-01' }
|
45
|
+
)
|
46
|
+
|
47
|
+
@all_transient = AllTransientSerializationTest.new(
|
48
|
+
id: 'transient_test_1',
|
49
|
+
temp1: 'value1',
|
50
|
+
temp2: 'value2'
|
51
|
+
)
|
52
|
+
|
53
|
+
@aliased_test = AliasedSerializationTest.new(
|
54
|
+
id: 'aliased_test_1',
|
55
|
+
display_name: 'Display Name',
|
56
|
+
cache: 'cache_value',
|
57
|
+
data: { key: 'value' }
|
58
|
+
)
|
59
|
+
|
60
|
+
## to_h excludes transient fields
|
61
|
+
@hash_result = @serialization_test.to_h
|
62
|
+
@hash_result.keys.sort
|
63
|
+
#=> [:description, :email, :id, :metadata, :name]
|
64
|
+
|
65
|
+
## to_h includes all persistent fields
|
66
|
+
@hash_result.key?(:name)
|
67
|
+
#=> true
|
68
|
+
|
69
|
+
## to_h includes encrypted persistent fields
|
70
|
+
@hash_result.key?(:email)
|
71
|
+
#=> true
|
72
|
+
|
73
|
+
## to_h includes explicitly persistent fields
|
74
|
+
@hash_result.key?(:description)
|
75
|
+
#=> true
|
76
|
+
|
77
|
+
## to_h excludes transient fields from serialization
|
78
|
+
@hash_result.key?(:tryouts_cache_data)
|
79
|
+
#=> false
|
80
|
+
|
81
|
+
## to_h excludes all transient fields
|
82
|
+
@hash_result.key?(:temp_settings)
|
83
|
+
#=> false
|
84
|
+
|
85
|
+
## to_h serializes complex values correctly
|
86
|
+
@hash_result[:metadata]
|
87
|
+
#=:> String
|
88
|
+
|
89
|
+
## to_a excludes transient fields
|
90
|
+
@array_result = @serialization_test.to_a
|
91
|
+
@array_result.size
|
92
|
+
#=> 5
|
93
|
+
|
94
|
+
## to_a maintains field order for persistent fields only
|
95
|
+
SerializationCategoryTest.persistent_fields
|
96
|
+
#=> [:id, :name, :email, :description, :metadata]
|
97
|
+
|
98
|
+
## Save operation only persists persistent fields
|
99
|
+
@serialization_test.save
|
100
|
+
#=> true
|
101
|
+
|
102
|
+
## Refresh loads only persistent fields
|
103
|
+
@serialization_test.refresh!
|
104
|
+
@serialization_test.name
|
105
|
+
#=> "Test User"
|
106
|
+
|
107
|
+
## Transient field values are not persisted in redis
|
108
|
+
@serialization_test.tryouts_cache_data
|
109
|
+
#=> nil
|
110
|
+
|
111
|
+
## When refreshed, transient fields do not retain their in-memory values
|
112
|
+
@serialization_test.refresh!
|
113
|
+
@serialization_test.tryouts_cache_data # Should still be in memory but not from redis
|
114
|
+
#=> nil
|
115
|
+
|
116
|
+
## Field definitions are preserved during serialization
|
117
|
+
SerializationCategoryTest.field_types[:tryouts_cache_data].category
|
118
|
+
#=> :transient
|
119
|
+
|
120
|
+
## Persistent fields filtering works correctly
|
121
|
+
SerializationCategoryTest.persistent_fields.include?(:tryouts_cache_data)
|
122
|
+
#=> false
|
123
|
+
|
124
|
+
## All persistent fields are included in persistent_fields
|
125
|
+
SerializationCategoryTest.persistent_fields.include?(:email)
|
126
|
+
#=> true
|
127
|
+
|
128
|
+
## to_h with only id field when all others are transient
|
129
|
+
@all_transient.to_h
|
130
|
+
#=> { id: "transient_test_1" }
|
131
|
+
|
132
|
+
## to_a with only id field when all others are transient
|
133
|
+
@all_transient.to_a
|
134
|
+
#=> ["transient_test_1"]
|
135
|
+
|
136
|
+
## Aliased fields serialization uses original field names
|
137
|
+
@aliased_hash = @aliased_test.to_h
|
138
|
+
@aliased_hash.keys.sort
|
139
|
+
#=> [:id, :internal_name, :user_data]
|
140
|
+
|
141
|
+
## Aliased transient fields are excluded
|
142
|
+
@aliased_hash.key?(:temp_cache)
|
143
|
+
#=> false
|
144
|
+
|
145
|
+
## Serialization works with accessor methods through aliases
|
146
|
+
@aliased_test.display_name = 'Updated Name'
|
147
|
+
@aliased_test.to_h[:internal_name]
|
148
|
+
#=> "Updated Name"
|
149
|
+
|
150
|
+
## Clear fields respects field method map
|
151
|
+
@serialization_test.clear_fields!
|
152
|
+
@serialization_test.name
|
153
|
+
#=> nil
|
154
|
+
|
155
|
+
## Clear fields affects aliased methods correctly
|
156
|
+
@aliased_test.clear_fields!
|
157
|
+
@aliased_test.display_name
|
158
|
+
#=> nil
|
159
|
+
|
160
|
+
@serialization_test.destroy! rescue nil
|
161
|
+
@all_transient.destroy! rescue nil
|
162
|
+
@aliased_test.destroy! rescue nil
|
163
|
+
@serialization_test = nil
|
164
|
+
@all_transient = nil
|
165
|
+
@aliased_test = nil
|
@@ -1,11 +1,10 @@
|
|
1
1
|
# try/horreum/serialization_try.rb
|
2
2
|
|
3
|
-
require_relative '../../lib/familia'
|
4
3
|
require_relative '../helpers/test_helpers'
|
5
4
|
|
6
5
|
Familia.debug = false
|
7
6
|
|
8
|
-
@identifier = 'tryouts-28@onetimesecret.
|
7
|
+
@identifier = 'tryouts-28@onetimesecret.dev'
|
9
8
|
@customer = Customer.new @identifier
|
10
9
|
|
11
10
|
## Basic save functionality works
|
@@ -23,7 +22,7 @@ Familia.debug = false
|
|
23
22
|
|
24
23
|
## to_h includes the custid field (using symbol keys)
|
25
24
|
@customer.to_h[:custid]
|
26
|
-
#=> "tryouts-28@onetimesecret.
|
25
|
+
#=> "tryouts-28@onetimesecret.dev"
|
27
26
|
|
28
27
|
## to_a returns field array in definition order
|
29
28
|
@customer.to_a.class
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# try/edge_cases/memory_try.rb
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
require_relative '../helpers/test_helpers'
|
7
|
+
|
8
|
+
class MemorySecurityTester
|
9
|
+
def self.test_redacted_string
|
10
|
+
results = {
|
11
|
+
timestamp: Time.now,
|
12
|
+
tests: []
|
13
|
+
}
|
14
|
+
|
15
|
+
# Test 1: Basic string search
|
16
|
+
secret = "SENSITIVE_#{rand(999999)}"
|
17
|
+
redacted = RedactedString.new(secret)
|
18
|
+
|
19
|
+
# Dump all strings to file
|
20
|
+
Tempfile.create('strings') do |f|
|
21
|
+
ObjectSpace.each_object(String) do |str|
|
22
|
+
f.puts str.inspect rescue nil
|
23
|
+
end
|
24
|
+
f.flush
|
25
|
+
|
26
|
+
# Check if secret appears
|
27
|
+
f.rewind
|
28
|
+
content = f.read
|
29
|
+
results[:tests] << {
|
30
|
+
name: "Basic string search",
|
31
|
+
passed: !content.include?(secret),
|
32
|
+
details: content.include?(secret) ? "Found secret in object space" : "Secret not found"
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Test 2: Memory after GC
|
37
|
+
redacted.clear!
|
38
|
+
GC.start(full_mark: true, immediate_sweep: true)
|
39
|
+
sleep 0.1
|
40
|
+
|
41
|
+
found = false
|
42
|
+
ObjectSpace.each_object(String) do |str|
|
43
|
+
found = true if str.include?(secret) rescue false
|
44
|
+
end
|
45
|
+
|
46
|
+
results[:tests] << {
|
47
|
+
name: "After clear and GC",
|
48
|
+
passed: !found,
|
49
|
+
details: found ? "Secret persists after clear" : "Secret cleared"
|
50
|
+
}
|
51
|
+
|
52
|
+
# Test 3: Check /proc/self/mem directly
|
53
|
+
begin
|
54
|
+
mem_content = File.read("/proc/self/mem", 1024*1024*10) rescue ""
|
55
|
+
results[:tests] << {
|
56
|
+
name: "Direct memory read",
|
57
|
+
passed: !mem_content.include?(secret),
|
58
|
+
details: mem_content.include?(secret) ? "Found in /proc/self/mem" : "Not in readable memory"
|
59
|
+
}
|
60
|
+
rescue => e
|
61
|
+
results[:tests] << {
|
62
|
+
name: "Direct memory read",
|
63
|
+
passed: nil,
|
64
|
+
details: "Could not read: #{e}"
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
puts JSON.pretty_generate(results)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Run the test
|
73
|
+
MemorySecurityTester.test_redacted_string
|