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.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +3 -3
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +18 -3
  7. data/README.md +36 -157
  8. data/TEST_COVERAGE.md +40 -0
  9. data/docs/overview.md +359 -0
  10. data/docs/wiki/API-Reference.md +270 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
  12. data/docs/wiki/Home.md +49 -0
  13. data/docs/wiki/Implementation-Guide.md +183 -0
  14. data/docs/wiki/Security-Model.md +143 -0
  15. data/lib/familia/base.rb +18 -27
  16. data/lib/familia/connection.rb +6 -5
  17. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  18. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  19. data/lib/familia/{datatype → data_type}/types/hashkey.rb +2 -2
  20. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  21. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  22. data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
  23. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  24. data/lib/familia/{datatype.rb → data_type.rb} +10 -12
  25. data/lib/familia/encryption/manager.rb +102 -0
  26. data/lib/familia/encryption/provider.rb +49 -0
  27. data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
  28. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  29. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -0
  30. data/lib/familia/encryption/registry.rb +50 -0
  31. data/lib/familia/encryption.rb +178 -0
  32. data/lib/familia/encryption_request_cache.rb +68 -0
  33. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +153 -0
  34. data/lib/familia/features/encrypted_fields.rb +28 -0
  35. data/lib/familia/features/expiration.rb +107 -77
  36. data/lib/familia/features/quantization.rb +5 -9
  37. data/lib/familia/features/relatable_objects.rb +2 -4
  38. data/lib/familia/features/safe_dump.rb +14 -17
  39. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  40. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  41. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  42. data/lib/familia/features/transient_fields.rb +47 -0
  43. data/lib/familia/features.rb +40 -24
  44. data/lib/familia/field_type.rb +270 -0
  45. data/lib/familia/horreum/connection.rb +8 -11
  46. data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
  47. data/lib/familia/horreum/definition_methods.rb +453 -0
  48. data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
  49. data/lib/familia/horreum/serialization.rb +46 -18
  50. data/lib/familia/horreum/settings.rb +10 -2
  51. data/lib/familia/horreum/utils.rb +9 -10
  52. data/lib/familia/horreum.rb +18 -10
  53. data/lib/familia/logging.rb +14 -14
  54. data/lib/familia/settings.rb +39 -3
  55. data/lib/familia/utils.rb +45 -0
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -1
  58. data/try/core/base_enhancements_try.rb +115 -0
  59. data/try/core/connection_try.rb +0 -1
  60. data/try/core/errors_try.rb +0 -1
  61. data/try/core/familia_extended_try.rb +3 -4
  62. data/try/core/familia_try.rb +0 -1
  63. data/try/core/pools_try.rb +2 -2
  64. data/try/core/secure_identifier_try.rb +0 -1
  65. data/try/core/settings_try.rb +0 -1
  66. data/try/core/utils_try.rb +0 -1
  67. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  68. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  69. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  70. data/try/{datatypes → data_types}/list_try.rb +1 -2
  71. data/try/{datatypes → data_types}/set_try.rb +1 -2
  72. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  73. data/try/{datatypes → data_types}/string_try.rb +1 -2
  74. data/try/debugging/README.md +32 -0
  75. data/try/debugging/cache_behavior_tracer.rb +91 -0
  76. data/try/debugging/encryption_method_tracer.rb +138 -0
  77. data/try/debugging/provider_diagnostics.rb +110 -0
  78. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  79. data/try/edge_cases/json_serialization_try.rb +0 -1
  80. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  81. data/try/encryption/config_persistence_try.rb +192 -0
  82. data/try/encryption/encryption_core_try.rb +328 -0
  83. data/try/encryption/instance_variable_scope_try.rb +31 -0
  84. data/try/encryption/module_loading_try.rb +28 -0
  85. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  86. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  87. data/try/encryption/roundtrip_validation_try.rb +28 -0
  88. data/try/encryption/secure_memory_handling_try.rb +125 -0
  89. data/try/features/encrypted_fields_core_try.rb +117 -0
  90. data/try/features/encrypted_fields_integration_try.rb +220 -0
  91. data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
  92. data/try/features/encrypted_fields_security_try.rb +370 -0
  93. data/try/features/encryption_fields/aad_protection_try.rb +53 -0
  94. data/try/features/encryption_fields/context_isolation_try.rb +120 -0
  95. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  96. data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
  97. data/try/features/encryption_fields/fresh_key_try.rb +163 -0
  98. data/try/features/encryption_fields/key_rotation_try.rb +117 -0
  99. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  100. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  101. data/try/features/encryption_fields/nonce_uniqueness_try.rb +54 -0
  102. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  103. data/try/features/expiration_try.rb +0 -1
  104. data/try/features/feature_dependencies_try.rb +159 -0
  105. data/try/features/quantization_try.rb +0 -1
  106. data/try/features/real_feature_integration_try.rb +148 -0
  107. data/try/features/relatable_objects_try.rb +0 -1
  108. data/try/features/safe_dump_advanced_try.rb +0 -1
  109. data/try/features/safe_dump_try.rb +0 -1
  110. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  111. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  112. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  113. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  114. data/try/features/transient_fields_core_try.rb +181 -0
  115. data/try/features/transient_fields_integration_try.rb +260 -0
  116. data/try/helpers/test_helpers.rb +42 -0
  117. data/try/horreum/base_try.rb +157 -3
  118. data/try/horreum/class_methods_try.rb +27 -36
  119. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  120. data/try/horreum/field_categories_try.rb +118 -0
  121. data/try/horreum/field_definition_try.rb +96 -0
  122. data/try/horreum/initialization_try.rb +0 -1
  123. data/try/horreum/relations_try.rb +0 -1
  124. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  125. data/try/horreum/serialization_try.rb +2 -3
  126. data/try/memory/memory_basic_test.rb +73 -0
  127. data/try/memory/memory_detailed_test.rb +121 -0
  128. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  129. data/try/memory/memory_search_for_string.rb +83 -0
  130. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  131. data/try/models/customer_safe_dump_try.rb +0 -1
  132. data/try/models/customer_try.rb +0 -1
  133. data/try/models/datatype_base_try.rb +1 -2
  134. data/try/models/familia_object_try.rb +0 -1
  135. 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
@@ -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
@@ -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
@@ -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.com'
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.com"
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