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,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
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# try/horreum/initialization_try.rb
|
2
2
|
|
3
|
-
require_relative '../../lib/familia'
|
4
3
|
require_relative '../helpers/test_helpers'
|
5
4
|
|
6
5
|
Familia.debug = false
|
@@ -50,7 +49,7 @@ Familia.debug = false
|
|
50
49
|
#=> ["mixed@test.com", "Mixed Test", nil, "user"]
|
51
50
|
|
52
51
|
## to_h works correctly with keyword-initialized objects
|
53
|
-
@customer2.to_h[
|
52
|
+
@customer2.to_h["name"]
|
54
53
|
#=> "Jane Smith"
|
55
54
|
|
56
55
|
## to_a works correctly with keyword-initialized objects
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# try/horreum/relations_try.rb
|
2
2
|
# Test Horreum Database type relations functionality
|
3
3
|
|
4
|
-
require_relative '../../lib/familia'
|
5
4
|
require_relative '../helpers/test_helpers'
|
6
5
|
|
7
6
|
Familia.debug = false
|
@@ -107,7 +106,7 @@ prefs = @test_user.preferences
|
|
107
106
|
@test_product.views.increment
|
108
107
|
@test_product.views.incrementby(5)
|
109
108
|
@test_product.views.value
|
110
|
-
#=>
|
109
|
+
#=> 6
|
111
110
|
|
112
111
|
## Database types maintain parent reference
|
113
112
|
@test_user.sessions.parent == @test_user
|
@@ -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
|
@@ -13,17 +12,46 @@ Familia.debug = false
|
|
13
12
|
@customer.save
|
14
13
|
#=> true
|
15
14
|
|
15
|
+
## save_if_not_exists saves new customer successfully
|
16
|
+
Familia.dbclient.set('debug:starting_save_if_not_exists_tests', Time.now.to_s)
|
17
|
+
@test_id = "#{Time.now.to_i}-#{rand(1000)}"
|
18
|
+
@new_customer = Customer.new "new-customer-#{@test_id}@test.com"
|
19
|
+
@new_customer.name = 'New Customer'
|
20
|
+
@new_customer.save_if_not_exists
|
21
|
+
#=> true
|
22
|
+
|
23
|
+
## save_if_not_exists raises error when customer already exists
|
24
|
+
@duplicate_customer = Customer.new "new-customer-#{@test_id}@test.com"
|
25
|
+
@duplicate_customer.name = 'Duplicate Customer'
|
26
|
+
@duplicate_customer.save_if_not_exists
|
27
|
+
#=!> Familia::RecordExistsError
|
28
|
+
#==> error.message.include?("Key already exists")
|
29
|
+
|
30
|
+
## save_if_not_exists with update_expiration: false works
|
31
|
+
@another_new_customer = Customer.new "another-new-#{@test_id}@test.com"
|
32
|
+
@another_new_customer.name = 'Another New'
|
33
|
+
@another_new_customer.save_if_not_exists(update_expiration: false)
|
34
|
+
#=> true
|
35
|
+
|
36
|
+
## End of save_if_not_exists tests
|
37
|
+
Familia.dbclient.set('debug:ending_save_if_not_exists_tests', Time.now.to_s)
|
38
|
+
|
39
|
+
## save_if_not_exists persists data correctly
|
40
|
+
@another_new_customer.refresh!
|
41
|
+
@another_new_customer.name
|
42
|
+
#=> "Another New"
|
43
|
+
|
16
44
|
## to_h returns field hash with all Customer fields
|
17
45
|
@customer.to_h.class
|
18
46
|
#=> Hash
|
19
47
|
|
20
|
-
## to_h includes the fields we set (using
|
21
|
-
@customer.to_h[
|
48
|
+
## to_h includes the fields we set (using string keys)
|
49
|
+
@customer.to_h["name"]
|
22
50
|
#=> "John Doe"
|
23
51
|
|
24
|
-
## to_h includes the custid field (using
|
25
|
-
@customer.to_h[
|
26
|
-
#=> "tryouts-28@onetimesecret.
|
52
|
+
## to_h includes the custid field (using string keys)
|
53
|
+
@customer.to_h["custid"]
|
54
|
+
#=> "tryouts-28@onetimesecret.dev"
|
27
55
|
|
28
56
|
## to_a returns field array in definition order
|
29
57
|
@customer.to_a.class
|
@@ -159,3 +187,9 @@ result.successful?
|
|
159
187
|
@fresh_customer.refresh!
|
160
188
|
[@fresh_customer.role, @fresh_customer.planid]
|
161
189
|
#=> ["admin", "premium"]
|
190
|
+
|
191
|
+
# Cleanup test data
|
192
|
+
[@customer, @new_customer, @another_new_customer, @fresh_customer].each do |obj|
|
193
|
+
next unless obj&.identifier && !obj.identifier.to_s.empty?
|
194
|
+
obj.destroy! if obj.exists?
|
195
|
+
end
|
@@ -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
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# try/edge_cases/memory_detailed_test_try.rb
|
2
|
+
|
3
|
+
require 'objspace'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
require_relative '../helpers/test_helpers'
|
7
|
+
|
8
|
+
class DetailedMemoryTester
|
9
|
+
def self.test_with_details
|
10
|
+
ObjectSpace.trace_object_allocations_start
|
11
|
+
|
12
|
+
secret = "SENSITIVE_#{rand(999999)}_DATA"
|
13
|
+
puts "Testing with secret: #{secret}"
|
14
|
+
puts "Secret object_id: #{secret.object_id}"
|
15
|
+
puts "Secret frozen?: #{secret.frozen?}\n\n"
|
16
|
+
|
17
|
+
# Track all string copies
|
18
|
+
tracker = {}
|
19
|
+
|
20
|
+
# Before creating RedactedString
|
21
|
+
find_secret_copies(secret, "BEFORE RedactedString creation", tracker)
|
22
|
+
|
23
|
+
# Create RedactedString
|
24
|
+
redacted = RedactedString.new(secret)
|
25
|
+
find_secret_copies(secret, "AFTER RedactedString creation", tracker)
|
26
|
+
|
27
|
+
# Use expose block
|
28
|
+
exposed_value = nil
|
29
|
+
redacted.expose do |plain|
|
30
|
+
exposed_value = plain.object_id
|
31
|
+
find_secret_copies(secret, "DURING expose block", tracker)
|
32
|
+
end
|
33
|
+
find_secret_copies(secret, "AFTER expose block", tracker)
|
34
|
+
|
35
|
+
# Clear and GC
|
36
|
+
redacted.clear!
|
37
|
+
original_secret = secret
|
38
|
+
secret = nil # Remove our reference
|
39
|
+
GC.start(full_mark: true, immediate_sweep: true)
|
40
|
+
|
41
|
+
find_secret_copies(original_secret, "AFTER clear! and GC", tracker)
|
42
|
+
|
43
|
+
# Final report
|
44
|
+
puts "\n" + "="*60
|
45
|
+
puts "FINAL ANALYSIS"
|
46
|
+
puts "="*60
|
47
|
+
|
48
|
+
remaining_copies = []
|
49
|
+
ObjectSpace.each_object(String) do |str|
|
50
|
+
begin
|
51
|
+
if str.include?(original_secret)
|
52
|
+
remaining_copies << {
|
53
|
+
object_id: str.object_id,
|
54
|
+
size: str.bytesize,
|
55
|
+
encoding: str.encoding.name,
|
56
|
+
frozen: str.frozen?,
|
57
|
+
tainted: (str.tainted? rescue "N/A"),
|
58
|
+
value_preview: str[0..50]
|
59
|
+
}
|
60
|
+
end
|
61
|
+
rescue => e
|
62
|
+
# Skip strings that can't be accessed
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
if remaining_copies.empty?
|
67
|
+
puts "✅ SUCCESS: No copies found in memory!"
|
68
|
+
else
|
69
|
+
puts "❌ FAILURE: #{remaining_copies.size} copies still in memory:"
|
70
|
+
remaining_copies.each do |copy|
|
71
|
+
puts "\n Object ID: #{copy[:object_id]}"
|
72
|
+
puts " Size: #{copy[:size]} bytes"
|
73
|
+
puts " Frozen: #{copy[:frozen]}"
|
74
|
+
puts " Encoding: #{copy[:encoding]}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Show memory stats
|
79
|
+
puts "\n" + "="*60
|
80
|
+
puts "MEMORY STATISTICS"
|
81
|
+
puts "="*60
|
82
|
+
puts "Total strings in ObjectSpace: #{ObjectSpace.each_object(String).count}"
|
83
|
+
puts "GC count: #{GC.count}"
|
84
|
+
puts "GC stat: #{GC.stat[:heap_live_slots]} live slots"
|
85
|
+
|
86
|
+
tracker
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def self.find_secret_copies(secret, phase, tracker)
|
92
|
+
copies = []
|
93
|
+
|
94
|
+
ObjectSpace.each_object(String) do |str|
|
95
|
+
begin
|
96
|
+
if str.include?(secret)
|
97
|
+
copies << {
|
98
|
+
object_id: str.object_id,
|
99
|
+
frozen: str.frozen?,
|
100
|
+
source: ObjectSpace.allocation_sourcefile(str),
|
101
|
+
line: ObjectSpace.allocation_sourceline(str)
|
102
|
+
}
|
103
|
+
end
|
104
|
+
rescue => e
|
105
|
+
# Some strings might not be accessible
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
tracker[phase] = copies
|
110
|
+
|
111
|
+
puts "#{phase}: Found #{copies.size} copies"
|
112
|
+
copies.each do |copy|
|
113
|
+
source_info = copy[:source] ? "#{copy[:source]}:#{copy[:line]}" : "unknown source"
|
114
|
+
puts " - Object #{copy[:object_id]} (frozen: #{copy[:frozen]}) from #{source_info}"
|
115
|
+
end
|
116
|
+
puts ""
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Run the detailed test
|
121
|
+
DetailedMemoryTester.test_with_details
|