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
data/try/core/errors_try.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# try/core/errors_try.rb
|
2
2
|
|
3
|
-
require_relative '../../lib/familia'
|
4
3
|
require_relative '../helpers/test_helpers'
|
5
4
|
|
6
5
|
Familia.debug = false
|
@@ -73,21 +72,42 @@ end
|
|
73
72
|
begin
|
74
73
|
raise Familia::KeyNotFoundError.new('test:key')
|
75
74
|
rescue Familia::KeyNotFoundError => e
|
76
|
-
e.message.include?('Key not found
|
75
|
+
e.message.include?('Key not found')
|
77
76
|
end
|
78
77
|
#=> true
|
79
78
|
|
80
79
|
## KeyNotFoundError has custom message again
|
81
80
|
raise Familia::KeyNotFoundError.new('test:key')
|
82
|
-
#=!> error.message.include?("Key not found
|
81
|
+
#=!> error.message.include?("Key not found")
|
83
82
|
#=!> error.class == Familia::KeyNotFoundError
|
84
83
|
|
84
|
+
## RecordExistsError stores key
|
85
|
+
begin
|
86
|
+
raise Familia::RecordExistsError.new('existing:key')
|
87
|
+
rescue Familia::RecordExistsError => e
|
88
|
+
e.key
|
89
|
+
end
|
90
|
+
#=> "existing:key"
|
91
|
+
|
92
|
+
## RecordExistsError has custom message
|
93
|
+
begin
|
94
|
+
raise Familia::RecordExistsError.new('existing:key')
|
95
|
+
rescue Familia::RecordExistsError => e
|
96
|
+
e.message.include?('Key already exists')
|
97
|
+
end
|
98
|
+
#=> true
|
99
|
+
|
100
|
+
## RecordExistsError inherits from NonUniqueKey
|
101
|
+
Familia::RecordExistsError.superclass
|
102
|
+
#=> Familia::NonUniqueKey
|
103
|
+
|
85
104
|
## All error classes inherit from Problem
|
86
105
|
[
|
87
106
|
Familia::NoIdentifier,
|
88
107
|
Familia::NonUniqueKey,
|
89
108
|
Familia::HighRiskFactor,
|
90
109
|
Familia::NotConnected,
|
91
|
-
Familia::KeyNotFoundError
|
92
|
-
|
110
|
+
Familia::KeyNotFoundError,
|
111
|
+
Familia::RecordExistsError
|
112
|
+
].all? { |klass| klass.superclass == Familia::Problem || klass.superclass.superclass == Familia::Problem }
|
93
113
|
##=> true
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
require 'time'
|
4
4
|
|
5
|
-
require_relative '../../lib/familia'
|
6
5
|
require_relative '../helpers/test_helpers'
|
7
6
|
|
8
7
|
## Has all datatype relativess
|
@@ -11,15 +10,15 @@ registered_types.collect(&:to_s).sort
|
|
11
10
|
#=> ["counter", "hash", "hashkey", "list", "lock", "set", "sorted_set", "string", "zset"]
|
12
11
|
|
13
12
|
## Familia created class methods for datatype list class
|
14
|
-
Familia::Horreum::
|
13
|
+
Familia::Horreum::DefinitionMethods.public_method_defined? :list?
|
15
14
|
#=> true
|
16
15
|
|
17
16
|
## Familia created class methods for datatype list class
|
18
|
-
Familia::Horreum::
|
17
|
+
Familia::Horreum::DefinitionMethods.public_method_defined? :list
|
19
18
|
#=> true
|
20
19
|
|
21
20
|
## Familia created class methods for datatype list class
|
22
|
-
Familia::Horreum::
|
21
|
+
Familia::Horreum::DefinitionMethods.public_method_defined? :lists
|
23
22
|
#=> true
|
24
23
|
|
25
24
|
## A Familia object knows its datatype relatives
|
data/try/core/familia_try.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
# try/core/familia_try.rb
|
2
2
|
|
3
|
-
require_relative '../../lib/familia'
|
4
3
|
require_relative '../helpers/test_helpers'
|
5
4
|
|
6
5
|
## Check for help class
|
7
6
|
Bone.related_fields.keys # consistent b/c hashes are ordered
|
8
|
-
#=> [:owners, :tags, :metrics, :props, :value]
|
7
|
+
#=> [:owners, :tags, :metrics, :props, :value, :counter, :lock]
|
9
8
|
|
10
9
|
## Familia has a uri
|
11
10
|
Familia.uri
|
@@ -0,0 +1,297 @@
|
|
1
|
+
# try/core/persistence_operations_try.rb
|
2
|
+
#
|
3
|
+
# Comprehensive test coverage for core persistence methods: exists?, save, save_if_not_exists, create
|
4
|
+
# This test addresses gaps that allowed the exists? bug to go undetected
|
5
|
+
|
6
|
+
require_relative '../helpers/test_helpers'
|
7
|
+
|
8
|
+
# Use a simple test class to isolate persistence behavior
|
9
|
+
class PersistenceTestModel < Familia::Horreum
|
10
|
+
identifier_field :id
|
11
|
+
field :id
|
12
|
+
field :name
|
13
|
+
field :value
|
14
|
+
end
|
15
|
+
|
16
|
+
# Clean up any existing test data
|
17
|
+
cleanup_keys = []
|
18
|
+
begin
|
19
|
+
existing_test_keys = Familia.dbclient.keys('persistencetestmodel:*')
|
20
|
+
cleanup_keys.concat(existing_test_keys)
|
21
|
+
Familia.dbclient.del(*existing_test_keys) if existing_test_keys.any?
|
22
|
+
rescue => e
|
23
|
+
# Ignore cleanup errors
|
24
|
+
end
|
25
|
+
|
26
|
+
@test_id_counter = 0
|
27
|
+
def next_test_id
|
28
|
+
@test_id_counter += 1
|
29
|
+
"test-#{Time.now.to_i}-#{@test_id_counter}"
|
30
|
+
end
|
31
|
+
|
32
|
+
# =============================================
|
33
|
+
# 1. exists? Method Coverage - The Critical Bug
|
34
|
+
# =============================================
|
35
|
+
|
36
|
+
## New object does not exist (both variants)
|
37
|
+
@new_obj = PersistenceTestModel.new(id: next_test_id, name: 'New Object')
|
38
|
+
[@new_obj.exists?, @new_obj.exists?(check_size: false)]
|
39
|
+
#=> [false, false]
|
40
|
+
|
41
|
+
## Object exists after save (both variants)
|
42
|
+
@new_obj.save
|
43
|
+
[@new_obj.exists?, @new_obj.exists?(check_size: false)]
|
44
|
+
#=> [true, true]
|
45
|
+
|
46
|
+
## Class-level and instance-level exists? consistency
|
47
|
+
class_exists = PersistenceTestModel.exists?(@new_obj.identifier)
|
48
|
+
instance_exists = @new_obj.exists?
|
49
|
+
[class_exists, instance_exists]
|
50
|
+
#=> [true, true]
|
51
|
+
|
52
|
+
## Empty object exists check (critical edge case)
|
53
|
+
@empty_obj = PersistenceTestModel.new(id: next_test_id)
|
54
|
+
@empty_obj.save # Save with no fields set
|
55
|
+
# Should return true with check_size: false (key exists)
|
56
|
+
# Should return false with check_size: true (but fields exist due to id)
|
57
|
+
[@empty_obj.exists?(check_size: false), @empty_obj.exists?(check_size: true)]
|
58
|
+
#=> [true, true]
|
59
|
+
|
60
|
+
## Object with only nil fields edge case
|
61
|
+
@nil_fields_obj = PersistenceTestModel.new(id: next_test_id, name: nil, value: nil)
|
62
|
+
@nil_fields_obj.save
|
63
|
+
# Should handle nil fields correctly
|
64
|
+
[@nil_fields_obj.exists?(check_size: false), @nil_fields_obj.exists?(check_size: true)]
|
65
|
+
#=> [true, true]
|
66
|
+
|
67
|
+
## Object destroyed does not exist (both variants)
|
68
|
+
@new_obj.destroy!
|
69
|
+
[@new_obj.exists?, @new_obj.exists?(check_size: false)]
|
70
|
+
#=> [false, false]
|
71
|
+
|
72
|
+
# =============================================
|
73
|
+
# 2. save Method Coverage
|
74
|
+
# =============================================
|
75
|
+
|
76
|
+
## Basic save functionality
|
77
|
+
@save_test = PersistenceTestModel.new(id: next_test_id, name: 'Save Test', value: 'data')
|
78
|
+
result = @save_test.save
|
79
|
+
[result, @save_test.exists?]
|
80
|
+
#=> [true, true]
|
81
|
+
|
82
|
+
## Save with update_expiration: false
|
83
|
+
@save_no_exp = PersistenceTestModel.new(id: next_test_id, name: 'No Expiration')
|
84
|
+
result = @save_no_exp.save(update_expiration: false)
|
85
|
+
[result, @save_no_exp.exists?]
|
86
|
+
#=> [true, true]
|
87
|
+
|
88
|
+
## Save operation idempotency (multiple saves)
|
89
|
+
@idempotent_obj = PersistenceTestModel.new(id: next_test_id, name: 'Idempotent')
|
90
|
+
first_save = @idempotent_obj.save
|
91
|
+
@idempotent_obj.name = 'Modified'
|
92
|
+
second_save = @idempotent_obj.save
|
93
|
+
[first_save, second_save, @idempotent_obj.exists?]
|
94
|
+
#=> [true, true, true]
|
95
|
+
|
96
|
+
## Save with partial field data
|
97
|
+
@partial_obj = PersistenceTestModel.new(id: next_test_id)
|
98
|
+
@partial_obj.name = 'Only Name Set'
|
99
|
+
# value field is nil/unset
|
100
|
+
result = @partial_obj.save
|
101
|
+
[result, @partial_obj.exists?, @partial_obj.name]
|
102
|
+
#=> [true, true, 'Only Name Set']
|
103
|
+
|
104
|
+
# =============================================
|
105
|
+
# 3. save_if_not_exists Method Coverage
|
106
|
+
# =============================================
|
107
|
+
|
108
|
+
## save_if_not_exists saves new object successfully
|
109
|
+
@sine_new = PersistenceTestModel.new(id: next_test_id, name: 'Save If Not Exists New')
|
110
|
+
result = @sine_new.save_if_not_exists
|
111
|
+
[result, @sine_new.exists?]
|
112
|
+
#=> [true, true]
|
113
|
+
|
114
|
+
## save_if_not_exists raises error for existing object
|
115
|
+
@sine_duplicate = PersistenceTestModel.new(id: @sine_new.identifier, name: 'Duplicate')
|
116
|
+
@sine_duplicate.save_if_not_exists
|
117
|
+
#=!> Familia::RecordExistsError
|
118
|
+
|
119
|
+
## save_if_not_exists with update_expiration: false
|
120
|
+
@sine_no_exp = PersistenceTestModel.new(id: next_test_id, name: 'No Exp SINE')
|
121
|
+
result = @sine_no_exp.save_if_not_exists(update_expiration: false)
|
122
|
+
[result, @sine_no_exp.exists?]
|
123
|
+
#=> [true, true]
|
124
|
+
|
125
|
+
## Object state unchanged after save_if_not_exists failure
|
126
|
+
original_name = 'Original Name'
|
127
|
+
@sine_fail_test = PersistenceTestModel.new(id: next_test_id, name: original_name)
|
128
|
+
@sine_fail_test.save_if_not_exists
|
129
|
+
# Now create duplicate and verify state doesn't change on failure
|
130
|
+
@sine_fail_duplicate = PersistenceTestModel.new(id: @sine_fail_test.identifier, name: 'Changed Name')
|
131
|
+
begin
|
132
|
+
@sine_fail_duplicate.save_if_not_exists
|
133
|
+
false # Should not reach here
|
134
|
+
rescue Familia::RecordExistsError
|
135
|
+
# State should be unchanged
|
136
|
+
@sine_fail_duplicate.name == 'Changed Name'
|
137
|
+
end
|
138
|
+
#=> true
|
139
|
+
|
140
|
+
# =============================================
|
141
|
+
# 4. create Method Coverage (MISSING from current tests)
|
142
|
+
# =============================================
|
143
|
+
|
144
|
+
# NOTE: create method tests disabled due to Redis::Future bug
|
145
|
+
# This would be high-priority coverage but needs the create method bug fixed first
|
146
|
+
|
147
|
+
## create method alternative: manual creation simulation
|
148
|
+
@manual_created = PersistenceTestModel.new(id: next_test_id, name: 'Manual Created', value: 'manual')
|
149
|
+
before_create = @manual_created.exists?
|
150
|
+
if @manual_created.exists?
|
151
|
+
raise Familia::Problem, "Object already exists"
|
152
|
+
else
|
153
|
+
@manual_created.save
|
154
|
+
end
|
155
|
+
after_create = @manual_created.exists?
|
156
|
+
[before_create, after_create, @manual_created.name]
|
157
|
+
#=> [false, true, 'Manual Created']
|
158
|
+
|
159
|
+
## create duplicate prevention simulation
|
160
|
+
@duplicate_test = PersistenceTestModel.new(id: @manual_created.identifier, name: 'Duplicate Attempt')
|
161
|
+
begin
|
162
|
+
if @duplicate_test.exists?
|
163
|
+
raise Familia::Problem, "Object already exists"
|
164
|
+
else
|
165
|
+
@duplicate_test.save
|
166
|
+
end
|
167
|
+
false # Should not reach here
|
168
|
+
rescue Familia::Problem
|
169
|
+
true # Expected
|
170
|
+
end
|
171
|
+
#=> true
|
172
|
+
|
173
|
+
# =============================================
|
174
|
+
# 5. State Transition Testing (Critical Gap)
|
175
|
+
# =============================================
|
176
|
+
|
177
|
+
## NEW → SAVED: Verify exists? changes from false to true
|
178
|
+
@state_obj = PersistenceTestModel.new(id: next_test_id, name: 'State Transition')
|
179
|
+
@before_save = @state_obj.exists?
|
180
|
+
@state_obj.save
|
181
|
+
@after_save = @state_obj.exists?
|
182
|
+
[@before_save, @after_save]
|
183
|
+
#=> [false, true]
|
184
|
+
|
185
|
+
## SAVED → DESTROYED: Verify exists? changes from true to false
|
186
|
+
# Use the same state object from previous test
|
187
|
+
@state_obj.destroy!
|
188
|
+
@after_destroy = @state_obj.exists?
|
189
|
+
[@after_save, @after_destroy] # Use instance variables
|
190
|
+
#=> [true, false]
|
191
|
+
|
192
|
+
## SAVED → MODIFIED → SAVED: State consistency through updates
|
193
|
+
@mod_obj = PersistenceTestModel.new(id: next_test_id, name: 'Original', value: 'original_val')
|
194
|
+
@mod_obj.save
|
195
|
+
original_exists = @mod_obj.exists?
|
196
|
+
@mod_obj.name = 'Modified'
|
197
|
+
@mod_obj.value = 'modified_val'
|
198
|
+
@mod_obj.save
|
199
|
+
modified_exists = @mod_obj.exists?
|
200
|
+
# Refresh to verify persistence
|
201
|
+
@mod_obj.refresh!
|
202
|
+
persisted_name = @mod_obj.name
|
203
|
+
[original_exists, modified_exists, persisted_name]
|
204
|
+
#=> [true, true, 'Modified']
|
205
|
+
|
206
|
+
## Field persistence across state changes
|
207
|
+
@field_obj = PersistenceTestModel.new(id: next_test_id)
|
208
|
+
# Start with no name
|
209
|
+
@field_obj.save
|
210
|
+
@field_obj.name = 'Added Later'
|
211
|
+
@field_obj.save
|
212
|
+
@field_obj.refresh!
|
213
|
+
@field_obj.name
|
214
|
+
#=> 'Added Later'
|
215
|
+
|
216
|
+
# =============================================
|
217
|
+
# 6. Integration with Features
|
218
|
+
# =============================================
|
219
|
+
|
220
|
+
## exists? behavior with encrypted fields (if available)
|
221
|
+
test_keys = {
|
222
|
+
v1: Base64.strict_encode64('a' * 32),
|
223
|
+
}
|
224
|
+
Familia.config.encryption_keys = test_keys
|
225
|
+
Familia.config.current_key_version = :v1
|
226
|
+
|
227
|
+
class EncryptedPersistenceTest < Familia::Horreum
|
228
|
+
feature :encrypted_fields
|
229
|
+
identifier_field :id
|
230
|
+
field :id
|
231
|
+
field :email
|
232
|
+
encrypted_field :secret_value
|
233
|
+
end
|
234
|
+
|
235
|
+
@enc_obj = EncryptedPersistenceTest.new(id: next_test_id, email: 'test@example.com')
|
236
|
+
before_save = @enc_obj.exists?
|
237
|
+
@enc_obj.save
|
238
|
+
@enc_obj.secret_value = 'encrypted_data'
|
239
|
+
@enc_obj.save
|
240
|
+
after_save = @enc_obj.exists?
|
241
|
+
|
242
|
+
# Clean up encryption config
|
243
|
+
Familia.config.encryption_keys = nil
|
244
|
+
Familia.config.current_key_version = nil
|
245
|
+
|
246
|
+
[before_save, after_save]
|
247
|
+
#=> [false, true]
|
248
|
+
|
249
|
+
# =============================================
|
250
|
+
# 7. Error Handling & Edge Cases
|
251
|
+
# =============================================
|
252
|
+
|
253
|
+
## Empty identifier handling
|
254
|
+
begin
|
255
|
+
empty_id_obj = PersistenceTestModel.new(id: '')
|
256
|
+
PersistenceTestModel.exists?('')
|
257
|
+
false # Should not reach here
|
258
|
+
rescue Familia::NoIdentifier
|
259
|
+
true # Expected error
|
260
|
+
end
|
261
|
+
#=> true
|
262
|
+
|
263
|
+
## nil identifier handling
|
264
|
+
begin
|
265
|
+
nil_id_obj = PersistenceTestModel.new(id: nil)
|
266
|
+
PersistenceTestModel.exists?(nil)
|
267
|
+
false # Should not reach here
|
268
|
+
rescue Familia::NoIdentifier
|
269
|
+
true # Expected error
|
270
|
+
end
|
271
|
+
#=> true
|
272
|
+
|
273
|
+
## Concurrent exists? checks are consistent
|
274
|
+
@concurrent_obj = PersistenceTestModel.new(id: next_test_id, name: 'Concurrent Test')
|
275
|
+
@concurrent_obj.save
|
276
|
+
|
277
|
+
# Multiple exists? calls should be consistent
|
278
|
+
results = 3.times.map { @concurrent_obj.exists? }
|
279
|
+
results.uniq.length
|
280
|
+
#=> 1
|
281
|
+
|
282
|
+
## Database key structure validation
|
283
|
+
@key_obj = PersistenceTestModel.new(id: next_test_id)
|
284
|
+
@key_obj.save
|
285
|
+
expected_suffix = ":#{@key_obj.identifier}:object"
|
286
|
+
actual_key = @key_obj.dbkey
|
287
|
+
[actual_key.include?(expected_suffix), @key_obj.exists?]
|
288
|
+
#=> [true, true]
|
289
|
+
|
290
|
+
# =============================================
|
291
|
+
# Cleanup
|
292
|
+
# =============================================
|
293
|
+
|
294
|
+
# Clean up test data
|
295
|
+
test_keys = Familia.dbclient.keys('persistencetestmodel:*')
|
296
|
+
test_keys.concat(Familia.dbclient.keys('encryptedpersistencetest:*')) if defined?(EncryptedPersistenceTest)
|
297
|
+
Familia.dbclient.del(*test_keys) if test_keys.any?
|
data/try/core/pools_try.rb
CHANGED
@@ -30,7 +30,7 @@ end
|
|
30
30
|
class PoolTestAccount < Familia::Horreum
|
31
31
|
identifier_field :account_id
|
32
32
|
field :account_id
|
33
|
-
field :balance
|
33
|
+
field :balance, on_conflict: :skip
|
34
34
|
field :holder_name
|
35
35
|
|
36
36
|
def init
|
@@ -83,7 +83,7 @@ class PoolTestAccountDB1 < Familia::Horreum
|
|
83
83
|
self.logical_database = 1
|
84
84
|
identifier_field :account_id
|
85
85
|
field :account_id
|
86
|
-
field :balance
|
86
|
+
field :balance, on_conflict: :skip
|
87
87
|
field :holder_name
|
88
88
|
|
89
89
|
def init
|
data/try/core/settings_try.rb
CHANGED
data/try/core/utils_try.rb
CHANGED
@@ -0,0 +1,93 @@
|
|
1
|
+
# try/data_types/counter_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
@a = Bone.new(token: 'atoken3')
|
6
|
+
|
7
|
+
## Bone#dbkey
|
8
|
+
@a.dbkey
|
9
|
+
#=> 'bone:atoken3:object'
|
10
|
+
|
11
|
+
## Familia::Counter should have default value of 0
|
12
|
+
@a.counter.value
|
13
|
+
#=> 0
|
14
|
+
|
15
|
+
## Familia::Counter#value=
|
16
|
+
@a.counter.value = 42
|
17
|
+
#=> 42
|
18
|
+
|
19
|
+
## Familia::Counter#to_i
|
20
|
+
@a.counter.to_i
|
21
|
+
#=> 42
|
22
|
+
|
23
|
+
## Familia::Counter#to_s
|
24
|
+
@a.counter.to_s
|
25
|
+
#=> '42'
|
26
|
+
|
27
|
+
## Familia::Counter#increment
|
28
|
+
@a.counter.increment
|
29
|
+
#=> 43
|
30
|
+
|
31
|
+
## Familia::Counter#incrementby
|
32
|
+
@a.counter.incrementby(10)
|
33
|
+
#=> 53
|
34
|
+
|
35
|
+
## Familia::Counter#decrement
|
36
|
+
@a.counter.decrement
|
37
|
+
#=> 52
|
38
|
+
|
39
|
+
## Familia::Counter#decrementby
|
40
|
+
@a.counter.decrementby(5)
|
41
|
+
#=> 47
|
42
|
+
|
43
|
+
## Familia::Counter#reset with value
|
44
|
+
@a.counter.reset(100)
|
45
|
+
#=> true
|
46
|
+
|
47
|
+
## Familia::Counter#reset without value (defaults to 0)
|
48
|
+
@a.counter.reset
|
49
|
+
@a.counter.reset
|
50
|
+
@a.counter.value
|
51
|
+
#=> 0
|
52
|
+
|
53
|
+
## Familia::Counter#atomic_increment_and_get
|
54
|
+
@a.counter.atomic_increment_and_get(25)
|
55
|
+
#=> 25
|
56
|
+
|
57
|
+
## Familia::Counter#increment_if_less_than (success case)
|
58
|
+
@a.counter.increment_if_less_than(50, 10)
|
59
|
+
#=> true
|
60
|
+
|
61
|
+
## Familia::Counter#value after conditional increment
|
62
|
+
@a.counter.to_i
|
63
|
+
#=> 35
|
64
|
+
|
65
|
+
## Familia::Counter#increment_if_less_than (failure case)
|
66
|
+
@a.counter.increment_if_less_than(30, 10)
|
67
|
+
#=> false
|
68
|
+
|
69
|
+
## Familia::Counter#value unchanged after failed conditional increment
|
70
|
+
@a.counter.to_i
|
71
|
+
#=> 35
|
72
|
+
|
73
|
+
## Familia::Counter.new standalone
|
74
|
+
@counter = Familia::Counter.new 'test:counter'
|
75
|
+
@counter.dbkey
|
76
|
+
#=> 'test:counter'
|
77
|
+
|
78
|
+
## Standalone counter starts at 0
|
79
|
+
@counter.value
|
80
|
+
#=> 0
|
81
|
+
|
82
|
+
## Standalone counter increment
|
83
|
+
@counter.increment
|
84
|
+
#=> 1
|
85
|
+
|
86
|
+
## Standalone counter set string value gets coerced to integer
|
87
|
+
@counter.value = "123"
|
88
|
+
@counter.to_i
|
89
|
+
#=> 123
|
90
|
+
|
91
|
+
# Cleanup
|
92
|
+
@a.counter.delete!
|
93
|
+
@counter.delete!
|
@@ -1,6 +1,5 @@
|
|
1
|
-
# try/
|
1
|
+
# try/data_types/base_try.rb
|
2
2
|
|
3
|
-
require_relative '../../lib/familia'
|
4
3
|
require_relative '../helpers/test_helpers'
|
5
4
|
|
6
5
|
@limiter1 = Limiter.new :requests
|
@@ -64,6 +63,6 @@ p [@limiter1.counter.parent.default_expiration, @limiter2.counter.parent.default
|
|
64
63
|
#=> 3600.0
|
65
64
|
|
66
65
|
## Check current_expiration
|
67
|
-
sleep 1 #
|
66
|
+
sleep 1 # NOTE: Mocking time would be foolish in life, but helpful here
|
68
67
|
@limiter1.counter.current_expiration
|
69
68
|
#=> 3600-1
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# try/data_types/lock_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
@a = Bone.new(token: 'atoken4')
|
6
|
+
|
7
|
+
## Bone#dbkey
|
8
|
+
@a.dbkey
|
9
|
+
#=> 'bone:atoken4:object'
|
10
|
+
|
11
|
+
## Familia::Lock should start unlocked
|
12
|
+
@a.lock.locked?
|
13
|
+
#=> false
|
14
|
+
|
15
|
+
## Familia::Lock#value should be nil when unlocked
|
16
|
+
@a.lock.value
|
17
|
+
#=> nil
|
18
|
+
|
19
|
+
## Familia::Lock#acquire returns token when successful
|
20
|
+
@token1 = @a.lock.acquire
|
21
|
+
@token1.class
|
22
|
+
#=> String
|
23
|
+
|
24
|
+
## Familia::Lock#locked? after acquire
|
25
|
+
@a.lock.locked?
|
26
|
+
#=> true
|
27
|
+
|
28
|
+
## Familia::Lock#held_by? with correct token
|
29
|
+
@a.lock.held_by?(@token1)
|
30
|
+
#=> true
|
31
|
+
|
32
|
+
## Familia::Lock#held_by? with wrong token
|
33
|
+
@a.lock.held_by?('wrong-token')
|
34
|
+
#=> false
|
35
|
+
|
36
|
+
## Familia::Lock#acquire when already locked returns false
|
37
|
+
@a.lock.acquire
|
38
|
+
#=> false
|
39
|
+
|
40
|
+
## Familia::Lock#release with correct token
|
41
|
+
@a.lock.release(@token1)
|
42
|
+
#=> true
|
43
|
+
|
44
|
+
## Familia::Lock#locked? after release
|
45
|
+
@a.lock.locked?
|
46
|
+
#=> false
|
47
|
+
|
48
|
+
## Familia::Lock#release with wrong token (lock not held)
|
49
|
+
@a.lock.release('wrong-token')
|
50
|
+
#=> false
|
51
|
+
|
52
|
+
## Familia::Lock#acquire with custom token
|
53
|
+
@custom_token = 'my-custom-token-123'
|
54
|
+
@result = @a.lock.acquire(@custom_token)
|
55
|
+
@result
|
56
|
+
#=> 'my-custom-token-123'
|
57
|
+
|
58
|
+
## Familia::Lock#held_by? with custom token
|
59
|
+
@a.lock.held_by?(@custom_token)
|
60
|
+
#=> true
|
61
|
+
|
62
|
+
## Familia::Lock#force_unlock!
|
63
|
+
@a.lock.force_unlock!
|
64
|
+
#=> true
|
65
|
+
|
66
|
+
## Familia::Lock#locked? after force unlock
|
67
|
+
@a.lock.locked?
|
68
|
+
#=> false
|
69
|
+
|
70
|
+
## Familia::Lock.new standalone
|
71
|
+
@lock = Familia::Lock.new 'test:lock'
|
72
|
+
@lock.dbkey
|
73
|
+
#=> 'test:lock'
|
74
|
+
|
75
|
+
## Standalone lock starts unlocked
|
76
|
+
@lock.locked?
|
77
|
+
#=> false
|
78
|
+
|
79
|
+
## Standalone lock acquire
|
80
|
+
@standalone_token = @lock.acquire
|
81
|
+
@standalone_token.class
|
82
|
+
#=> String
|
83
|
+
|
84
|
+
## Standalone lock is now locked
|
85
|
+
@lock.locked?
|
86
|
+
#=> true
|
87
|
+
|
88
|
+
## Standalone lock acquire with TTL
|
89
|
+
@lock.force_unlock!
|
90
|
+
@ttl_token = @lock.acquire('ttl-token', ttl: 1)
|
91
|
+
@ttl_token
|
92
|
+
#=> 'ttl-token'
|
93
|
+
|
94
|
+
## Wait for TTL expiration and check if lock auto-expires
|
95
|
+
# Note: This test might be flaky in fast test runs
|
96
|
+
sleep 2
|
97
|
+
@lock.locked?
|
98
|
+
#=> false
|
99
|
+
|
100
|
+
## Acquire with zero TTL should return false
|
101
|
+
@lock2 = Familia::Lock.new 'test:lock2'
|
102
|
+
@lock2.acquire('zero-ttl', ttl: 0)
|
103
|
+
#=> false
|
104
|
+
|
105
|
+
## Lock should not be held after zero TTL rejection
|
106
|
+
@lock2.locked?
|
107
|
+
#=> false
|
108
|
+
|
109
|
+
## Acquire with negative TTL should return false
|
110
|
+
@lock2.acquire('neg-ttl', ttl: -5)
|
111
|
+
#=> false
|
112
|
+
|
113
|
+
## Lock should not be held after negative TTL rejection
|
114
|
+
@lock2.locked?
|
115
|
+
#=> false
|
116
|
+
|
117
|
+
## Acquire with nil TTL should work (no expiration)
|
118
|
+
@nil_ttl_token = @lock2.acquire('no-expiry', ttl: nil)
|
119
|
+
@nil_ttl_token
|
120
|
+
#=> 'no-expiry'
|
121
|
+
|
122
|
+
## Lock with nil TTL should be held
|
123
|
+
@lock2.locked?
|
124
|
+
#=> true
|
125
|
+
|
126
|
+
## Lock with nil TTL should not have expiration
|
127
|
+
@lock2.current_expiration
|
128
|
+
#=> -1
|
129
|
+
|
130
|
+
## Cleanup
|
131
|
+
@a.lock.delete!
|
132
|
+
@lock.delete!
|
133
|
+
@lock2.delete!
|