familia 2.0.0.pre5 → 2.0.0.pre7
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/.github/workflows/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -10
- data/Gemfile +2 -2
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +631 -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 +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +9 -6
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -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 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -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/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- data/try/features/relatable_objects_try.rb +0 -220
data/try/models/customer_try.rb
CHANGED
@@ -43,7 +43,7 @@ Customer.find_by_id(ident).planid
|
|
43
43
|
@customer.secrets_created.delete!
|
44
44
|
@customer.secrets_created.increment
|
45
45
|
@customer.secrets_created.value
|
46
|
-
#=>
|
46
|
+
#=> 1
|
47
47
|
|
48
48
|
## Customer can add custom domain via add method
|
49
49
|
@customer.custom_domains.add(@now, 'example.org')
|
@@ -0,0 +1,320 @@
|
|
1
|
+
# Validation testing for atomic operations and transaction boundaries
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
require_relative '../../lib/familia/validation'
|
5
|
+
|
6
|
+
extend Familia::Validation::TestHelpers
|
7
|
+
|
8
|
+
# Test models for atomic validation
|
9
|
+
class AtomicTestAccount < Familia::Horreum
|
10
|
+
identifier_field :account_id
|
11
|
+
field :account_id
|
12
|
+
field :balance
|
13
|
+
field :status
|
14
|
+
|
15
|
+
def transfer_to(target_account, amount)
|
16
|
+
# This should be atomic - but let's test both atomic and non-atomic versions
|
17
|
+
new_balance = balance.to_i - amount.to_i
|
18
|
+
target_new_balance = target_account.balance.to_i + amount.to_i
|
19
|
+
|
20
|
+
# Non-atomic version (should fail validation)
|
21
|
+
self.balance = new_balance.to_s
|
22
|
+
target_account.balance = target_new_balance.to_s
|
23
|
+
save
|
24
|
+
target_account.save
|
25
|
+
end
|
26
|
+
|
27
|
+
def atomic_transfer_to(target_account, amount)
|
28
|
+
# Proper atomic version using transaction
|
29
|
+
new_balance = balance.to_i - amount.to_i
|
30
|
+
target_new_balance = target_account.balance.to_i + amount.to_i
|
31
|
+
|
32
|
+
transaction do |conn|
|
33
|
+
conn.hset(dbkey, 'balance', new_balance.to_s)
|
34
|
+
conn.hset(target_account.dbkey, 'balance', target_new_balance.to_s)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Update local state
|
38
|
+
self.balance = new_balance.to_s
|
39
|
+
target_account.balance = target_new_balance.to_s
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class AtomicTestCounter < Familia::Horreum
|
44
|
+
identifier_field :name
|
45
|
+
field :name
|
46
|
+
field :value
|
47
|
+
|
48
|
+
def increment_by(amount)
|
49
|
+
transaction do |conn|
|
50
|
+
conn.hincrby(dbkey, 'value', amount)
|
51
|
+
end
|
52
|
+
self.value = (value.to_i + amount).to_s
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
extend Familia::Validation::TestHelpers
|
57
|
+
setup_validation_test
|
58
|
+
|
59
|
+
## Clean up any existing test data
|
60
|
+
cleanup_keys = Familia.dbclient.keys("atomictestaccount:*")
|
61
|
+
cleanup_keys.concat(Familia.dbclient.keys("atomictestcounter:*"))
|
62
|
+
Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
|
63
|
+
true
|
64
|
+
#=> true
|
65
|
+
|
66
|
+
## Transaction expectations validate MULTI/EXEC boundaries
|
67
|
+
validator = Familia::Validation::Validator.new
|
68
|
+
result = validator.validate do |expect|
|
69
|
+
expect.transaction do |tx|
|
70
|
+
tx.hset("atomictestaccount:acc1:object", "balance", "900")
|
71
|
+
.hset("atomictestaccount:acc2:object", "balance", "1100")
|
72
|
+
end
|
73
|
+
|
74
|
+
# Execute atomic transfer
|
75
|
+
acc1 = AtomicTestAccount.new(account_id: "acc1", balance: "1000")
|
76
|
+
acc2 = AtomicTestAccount.new(account_id: "acc2", balance: "1000")
|
77
|
+
acc1.save
|
78
|
+
acc2.save
|
79
|
+
|
80
|
+
acc1.atomic_transfer_to(acc2, 100)
|
81
|
+
end
|
82
|
+
result.valid?
|
83
|
+
#=> true
|
84
|
+
|
85
|
+
## Non-atomic operations should fail strict atomicity validation
|
86
|
+
validator = Familia::Validation::Validator.new(strict_atomicity: true)
|
87
|
+
result = validator.validate do |expect|
|
88
|
+
expect.transaction do |tx|
|
89
|
+
tx.hset("atomictestaccount:acc3:object", "balance", "800")
|
90
|
+
.hset("atomictestaccount:acc4:object", "balance", "1200")
|
91
|
+
end
|
92
|
+
|
93
|
+
# This will NOT use transaction, so should fail
|
94
|
+
acc3 = AtomicTestAccount.new(account_id: "acc3", balance: "1000")
|
95
|
+
acc4 = AtomicTestAccount.new(account_id: "acc4", balance: "1000")
|
96
|
+
acc3.save
|
97
|
+
acc4.save
|
98
|
+
|
99
|
+
acc3.transfer_to(acc4, 200) # Non-atomic version
|
100
|
+
end
|
101
|
+
result.valid?
|
102
|
+
#=> false
|
103
|
+
|
104
|
+
## assert_atomic_operation helper works correctly
|
105
|
+
account_a = AtomicTestAccount.new(account_id: "acc5", balance: "2000")
|
106
|
+
account_b = AtomicTestAccount.new(account_id: "acc6", balance: "500")
|
107
|
+
account_a.save
|
108
|
+
account_b.save
|
109
|
+
|
110
|
+
assert_atomic_operation do |expect|
|
111
|
+
expect.transaction do |tx|
|
112
|
+
tx.hset("atomictestaccount:acc5:object", "balance", "1500")
|
113
|
+
.hset("atomictestaccount:acc6:object", "balance", "1000")
|
114
|
+
end
|
115
|
+
|
116
|
+
account_a.atomic_transfer_to(account_b, 500)
|
117
|
+
end
|
118
|
+
#=> true
|
119
|
+
|
120
|
+
## assert_transaction_used helper works
|
121
|
+
result = assert_transaction_used do
|
122
|
+
counter = AtomicTestCounter.new(name: "test_counter", value: "0")
|
123
|
+
counter.save
|
124
|
+
counter.increment_by(5)
|
125
|
+
end
|
126
|
+
result
|
127
|
+
#=> true
|
128
|
+
|
129
|
+
## assert_no_transaction_used helper works
|
130
|
+
result = assert_no_transaction_used do
|
131
|
+
simple_account = AtomicTestAccount.new(account_id: "simple", balance: "100")
|
132
|
+
simple_account.save # Just a save, no transaction needed
|
133
|
+
end
|
134
|
+
result
|
135
|
+
#=> true
|
136
|
+
|
137
|
+
## Transaction validation detects missing MULTI command
|
138
|
+
commands = []
|
139
|
+
commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
|
140
|
+
command: "HSET", args: ["key", "field", "value"],
|
141
|
+
result: "OK", timestamp: Time.now, duration_us: 100
|
142
|
+
)
|
143
|
+
commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
|
144
|
+
command: "EXEC", args: [],
|
145
|
+
result: ["OK"], timestamp: Time.now, duration_us: 50
|
146
|
+
)
|
147
|
+
|
148
|
+
sequence = Familia::Validation::CommandRecorder::CommandSequence.new
|
149
|
+
commands.each { |cmd| sequence.add_command(cmd) }
|
150
|
+
|
151
|
+
atomicity_validator = Familia::Validation::AtomicityValidator.new(sequence)
|
152
|
+
result = atomicity_validator.validate
|
153
|
+
result.valid?
|
154
|
+
#=> false
|
155
|
+
|
156
|
+
## Transaction validation detects missing EXEC command
|
157
|
+
commands = []
|
158
|
+
commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
|
159
|
+
command: "MULTI", args: [],
|
160
|
+
result: "OK", timestamp: Time.now, duration_us: 100
|
161
|
+
)
|
162
|
+
commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
|
163
|
+
command: "HSET", args: ["key", "field", "value"],
|
164
|
+
result: "QUEUED", timestamp: Time.now, duration_us: 50
|
165
|
+
)
|
166
|
+
|
167
|
+
sequence = Familia::Validation::CommandRecorder::CommandSequence.new
|
168
|
+
commands.each { |cmd| sequence.add_command(cmd) }
|
169
|
+
|
170
|
+
atomicity_validator = Familia::Validation::AtomicityValidator.new(sequence)
|
171
|
+
result = atomicity_validator.validate
|
172
|
+
result.valid?
|
173
|
+
#=> false
|
174
|
+
|
175
|
+
## Proper transaction structure passes validation
|
176
|
+
commands = []
|
177
|
+
commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
|
178
|
+
command: "MULTI", args: [],
|
179
|
+
result: "OK", timestamp: Time.now, duration_us: 100,
|
180
|
+
context: { transaction: false }
|
181
|
+
)
|
182
|
+
commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
|
183
|
+
command: "HSET", args: ["key", "field", "value"],
|
184
|
+
result: "QUEUED", timestamp: Time.now, duration_us: 50,
|
185
|
+
context: { transaction: true }
|
186
|
+
)
|
187
|
+
commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
|
188
|
+
command: "INCR", args: ["counter"],
|
189
|
+
result: "QUEUED", timestamp: Time.now, duration_us: 30,
|
190
|
+
context: { transaction: true }
|
191
|
+
)
|
192
|
+
commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
|
193
|
+
command: "EXEC", args: [],
|
194
|
+
result: [1, 1], timestamp: Time.now, duration_us: 200,
|
195
|
+
context: { transaction: false }
|
196
|
+
)
|
197
|
+
|
198
|
+
sequence = Familia::Validation::CommandRecorder::CommandSequence.new
|
199
|
+
sequence.start_transaction
|
200
|
+
commands.each { |cmd| sequence.add_command(cmd) }
|
201
|
+
sequence.end_transaction
|
202
|
+
|
203
|
+
atomicity_validator = Familia::Validation::AtomicityValidator.new(sequence)
|
204
|
+
result = atomicity_validator.validate
|
205
|
+
result.valid?
|
206
|
+
#=> true
|
207
|
+
|
208
|
+
## batch_update operations should be atomic
|
209
|
+
validator = Familia::Validation::Validator.new
|
210
|
+
result = validator.validate do |expect|
|
211
|
+
expect.transaction do |tx|
|
212
|
+
tx.hset("atomictestaccount:batch1:object", "balance", "1500")
|
213
|
+
.hset("atomictestaccount:batch1:object", "status", "active")
|
214
|
+
end
|
215
|
+
|
216
|
+
account = AtomicTestAccount.new(account_id: "batch1", balance: "1000", status: "pending")
|
217
|
+
account.save
|
218
|
+
|
219
|
+
# batch_update should use transaction internally
|
220
|
+
account.batch_update(balance: "1500", status: "active")
|
221
|
+
end
|
222
|
+
result.valid?
|
223
|
+
#=> true
|
224
|
+
|
225
|
+
## Nested transaction detection works
|
226
|
+
commands = []
|
227
|
+
# Invalid nested transaction
|
228
|
+
commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
|
229
|
+
command: "MULTI", args: [], result: "OK",
|
230
|
+
timestamp: Time.now, duration_us: 100
|
231
|
+
)
|
232
|
+
commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
|
233
|
+
command: "MULTI", args: [], result: "ERR",
|
234
|
+
timestamp: Time.now, duration_us: 50
|
235
|
+
)
|
236
|
+
|
237
|
+
sequence = Familia::Validation::CommandRecorder::CommandSequence.new
|
238
|
+
commands.each { |cmd| sequence.add_command(cmd) }
|
239
|
+
|
240
|
+
atomicity_validator = Familia::Validation::AtomicityValidator.new(sequence)
|
241
|
+
result = atomicity_validator.validate
|
242
|
+
result.valid?
|
243
|
+
#=> false
|
244
|
+
|
245
|
+
## Empty transaction detection
|
246
|
+
commands = []
|
247
|
+
commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
|
248
|
+
command: "MULTI", args: [], result: "OK",
|
249
|
+
timestamp: Time.now, duration_us: 100
|
250
|
+
)
|
251
|
+
commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
|
252
|
+
command: "EXEC", args: [], result: [],
|
253
|
+
timestamp: Time.now, duration_us: 50
|
254
|
+
)
|
255
|
+
|
256
|
+
sequence = Familia::Validation::CommandRecorder::CommandSequence.new
|
257
|
+
sequence.start_transaction
|
258
|
+
commands.each { |cmd| sequence.add_command(cmd) }
|
259
|
+
sequence.end_transaction
|
260
|
+
|
261
|
+
result = Familia::Validation::AtomicityValidator.new(sequence).validate
|
262
|
+
# Should be valid but with warning about empty transaction
|
263
|
+
result.valid? && result.warning_messages.any?
|
264
|
+
#=> true
|
265
|
+
|
266
|
+
## Real Familia transaction usage validates correctly
|
267
|
+
validator = Familia::Validation::Validator.new
|
268
|
+
result = validator.validate do |expect|
|
269
|
+
expect.transaction do |tx|
|
270
|
+
tx.hset("atomictestcounter:familia_tx:object", "value", "10")
|
271
|
+
end
|
272
|
+
|
273
|
+
counter = AtomicTestCounter.new(name: "familia_tx", value: "5")
|
274
|
+
counter.save
|
275
|
+
|
276
|
+
# Use Familia.transaction directly
|
277
|
+
Familia.transaction do |conn|
|
278
|
+
conn.hset(counter.dbkey, "value", "10")
|
279
|
+
end
|
280
|
+
end
|
281
|
+
result.valid?
|
282
|
+
#=> true
|
283
|
+
|
284
|
+
## Multiple transactions in sequence validate correctly
|
285
|
+
validator = Familia::Validation::Validator.new
|
286
|
+
result = validator.validate do |expect|
|
287
|
+
expect.transaction do |tx|
|
288
|
+
tx.hset("atomictestcounter:multi_tx_1:object", "value", "5")
|
289
|
+
end
|
290
|
+
|
291
|
+
expect.transaction do |tx|
|
292
|
+
tx.hset("atomictestcounter:multi_tx_2:object", "value", "10")
|
293
|
+
end
|
294
|
+
|
295
|
+
counter1 = AtomicTestCounter.new(name: "multi_tx_1", value: "0")
|
296
|
+
counter2 = AtomicTestCounter.new(name: "multi_tx_2", value: "0")
|
297
|
+
counter1.save
|
298
|
+
counter2.save
|
299
|
+
|
300
|
+
# Two separate transactions
|
301
|
+
Familia.transaction do |conn|
|
302
|
+
conn.hset(counter1.dbkey, "value", "5")
|
303
|
+
end
|
304
|
+
|
305
|
+
Familia.transaction do |conn|
|
306
|
+
conn.hset(counter2.dbkey, "value", "10")
|
307
|
+
end
|
308
|
+
end
|
309
|
+
result.valid?
|
310
|
+
#=> true
|
311
|
+
|
312
|
+
## Cleanup test environment
|
313
|
+
teardown_validation_test
|
314
|
+
|
315
|
+
## Clean up test data
|
316
|
+
cleanup_keys = Familia.dbclient.keys("atomictestaccount:*")
|
317
|
+
cleanup_keys.concat(Familia.dbclient.keys("atomictestcounter:*"))
|
318
|
+
Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
|
319
|
+
true
|
320
|
+
#=> true
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# Validation testing for Redis command recording and expectation matching
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
require_relative '../../lib/familia/validation'
|
5
|
+
|
6
|
+
# Test class for validation testing
|
7
|
+
class ValidationTestModel < Familia::Horreum
|
8
|
+
identifier_field :id
|
9
|
+
field :id
|
10
|
+
field :name
|
11
|
+
field :email
|
12
|
+
field :active
|
13
|
+
end
|
14
|
+
|
15
|
+
extend Familia::Validation::TestHelpers
|
16
|
+
|
17
|
+
# Initialize validation framework
|
18
|
+
setup_validation_test
|
19
|
+
|
20
|
+
## Command recorder captures basic Redis commands
|
21
|
+
Familia::Validation::CommandRecorder.start_recording
|
22
|
+
ValidationTestModel.new(id: "test1", name: "John").save
|
23
|
+
commands = Familia::Validation::CommandRecorder.stop_recording
|
24
|
+
commands.command_count > 0
|
25
|
+
#=> true
|
26
|
+
|
27
|
+
## Command recorder captures command details
|
28
|
+
Familia::Validation::CommandRecorder.start_recording
|
29
|
+
Familia.dbclient.hset("test:key", "field", "value")
|
30
|
+
commands = Familia::Validation::CommandRecorder.stop_recording
|
31
|
+
first_command = commands.commands.first
|
32
|
+
first_command ? [first_command.command, first_command.args] : "No commands recorded"
|
33
|
+
#=> ["HSET", ["test:key", "field", "value"]]
|
34
|
+
|
35
|
+
## Expectation DSL allows chaining commands
|
36
|
+
expectations = Familia::Validation::CommandExpectations.new
|
37
|
+
expectations.hset("user:123", "name", "John")
|
38
|
+
.hset("user:123", "email", "john@example.com")
|
39
|
+
.incr("counter")
|
40
|
+
expectations.expected_commands.length
|
41
|
+
#=> 3
|
42
|
+
|
43
|
+
## Basic command validation passes with exact match
|
44
|
+
validator = Familia::Validation::Validator.new
|
45
|
+
result = validator.validate do |expect|
|
46
|
+
expect.hset("validationtestmodel:test2:object", "name", "Jane")
|
47
|
+
.hset("validationtestmodel:test2:object", "id", "test2")
|
48
|
+
|
49
|
+
# Execute the actual operation
|
50
|
+
model = ValidationTestModel.new(id: "test2", name: "Jane")
|
51
|
+
model.save
|
52
|
+
end
|
53
|
+
result.valid?
|
54
|
+
#=> true
|
55
|
+
|
56
|
+
## Command validation fails with wrong command
|
57
|
+
validator = Familia::Validation::Validator.new
|
58
|
+
result = validator.validate do |expect|
|
59
|
+
expect.get("wrong:key") # This won't match actual operation
|
60
|
+
|
61
|
+
ValidationTestModel.new(id: "test3", name: "Bob").save
|
62
|
+
end
|
63
|
+
result.valid?
|
64
|
+
#=> false
|
65
|
+
|
66
|
+
## Command validation provides detailed error messages
|
67
|
+
validator = Familia::Validation::Validator.new
|
68
|
+
result = validator.validate do |expect|
|
69
|
+
expect.hset("wrong:key", "field", "value")
|
70
|
+
|
71
|
+
ValidationTestModel.new(id: "test4").save
|
72
|
+
end
|
73
|
+
result.error_messages.length > 0
|
74
|
+
#=> true
|
75
|
+
|
76
|
+
## Pattern matching works for flexible validation
|
77
|
+
validator = Familia::Validation::Validator.new
|
78
|
+
result = validator.validate do |expect|
|
79
|
+
expect.match_pattern(/^HSET validationtestmodel:test5:object/)
|
80
|
+
|
81
|
+
ValidationTestModel.new(id: "test5", name: "Alice").save
|
82
|
+
end
|
83
|
+
result.valid?
|
84
|
+
#=> true
|
85
|
+
|
86
|
+
## Any value matchers work correctly
|
87
|
+
validator = Familia::Validation::Validator.new
|
88
|
+
result = validator.validate do |expect|
|
89
|
+
expect.hset("validationtestmodel:test6:object", "name", any_string)
|
90
|
+
.hset("validationtestmodel:test6:object", "id", "test6")
|
91
|
+
|
92
|
+
ValidationTestModel.new(id: "test6", name: "Charlie").save
|
93
|
+
end
|
94
|
+
result.valid?
|
95
|
+
#=> true
|
96
|
+
|
97
|
+
## Test helper assert_redis_commands works
|
98
|
+
model = ValidationTestModel.new(id: "test7", name: "Dave")
|
99
|
+
assert_redis_commands do |expect|
|
100
|
+
expect.hset("validationtestmodel:test7:object", "name", "Dave")
|
101
|
+
.hset("validationtestmodel:test7:object", "id", "test7")
|
102
|
+
|
103
|
+
model.save
|
104
|
+
end
|
105
|
+
#=> true
|
106
|
+
|
107
|
+
## Test helper assert_command_count works
|
108
|
+
result = assert_command_count(2) do
|
109
|
+
Familia.dbclient.hset("test:count", "field1", "value1")
|
110
|
+
Familia.dbclient.hset("test:count", "field2", "value2")
|
111
|
+
end
|
112
|
+
result
|
113
|
+
#=> true
|
114
|
+
|
115
|
+
## Test helper assert_no_redis_commands works
|
116
|
+
result = assert_no_redis_commands do
|
117
|
+
# Just create object, don't save
|
118
|
+
ValidationTestModel.new(id: "test8", name: "Eve")
|
119
|
+
end
|
120
|
+
result
|
121
|
+
#=> true
|
122
|
+
|
123
|
+
## Flexible order validation works
|
124
|
+
validator = Familia::Validation::Validator.new
|
125
|
+
result = validator.validate do |expect|
|
126
|
+
expect.strict_order(false)
|
127
|
+
.hset("validationtestmodel:test9:object", "name", any_string)
|
128
|
+
.hset("validationtestmodel:test9:object", "id", "test9")
|
129
|
+
|
130
|
+
# Save in any order should work
|
131
|
+
model = ValidationTestModel.new(id: "test9", name: "Frank")
|
132
|
+
model.save
|
133
|
+
end
|
134
|
+
result.valid?
|
135
|
+
#=> true
|
136
|
+
|
137
|
+
## Command sequence provides useful metadata
|
138
|
+
Familia::Validation::CommandRecorder.start_recording
|
139
|
+
ValidationTestModel.new(id: "test10", name: "Grace").save
|
140
|
+
commands = Familia::Validation::CommandRecorder.stop_recording
|
141
|
+
summary = {
|
142
|
+
command_count: commands.command_count,
|
143
|
+
has_commands: commands.commands.any?,
|
144
|
+
first_command_type: commands.commands.first&.command_type
|
145
|
+
}
|
146
|
+
summary[:command_count] > 0 && summary[:has_commands]
|
147
|
+
#=> true
|
148
|
+
|
149
|
+
## Performance tracking captures timing information
|
150
|
+
validator = Familia::Validation::Validator.new(performance_tracking: true)
|
151
|
+
result = validator.validate do |expect|
|
152
|
+
expect.match_pattern(/HSET/)
|
153
|
+
|
154
|
+
ValidationTestModel.new(id: "test11", name: "Henry").save
|
155
|
+
end
|
156
|
+
result.respond_to?(:performance_metrics) && result.performance_metrics[:total_commands] > 0
|
157
|
+
#=> true
|
158
|
+
|
159
|
+
## Validation result provides comprehensive summary
|
160
|
+
validator = Familia::Validation::Validator.new
|
161
|
+
result = validator.validate do |expect|
|
162
|
+
expect.hset("validationtestmodel:test12:object", "name", "Iris")
|
163
|
+
|
164
|
+
ValidationTestModel.new(id: "test12", name: "Iris").save
|
165
|
+
end
|
166
|
+
summary = result.summary
|
167
|
+
summary[:valid] == true && summary[:expected_commands] == 1
|
168
|
+
#=> true
|
169
|
+
|
170
|
+
## Complex validation with multiple operations
|
171
|
+
class ComplexTestModel < Familia::Horreum
|
172
|
+
identifier_field :id
|
173
|
+
field :id, :name, :email
|
174
|
+
list :tags
|
175
|
+
set :categories
|
176
|
+
end
|
177
|
+
|
178
|
+
validator = Familia::Validation::Validator.new
|
179
|
+
result = validator.validate do |expect|
|
180
|
+
expect.hset("complextestmodel:complex1:object", "name", "Complex")
|
181
|
+
.hset("complextestmodel:complex1:object", "id", "complex1")
|
182
|
+
.lpush("complextestmodel:complex1:tags", "tag1")
|
183
|
+
.sadd("complextestmodel:complex1:categories", "cat1")
|
184
|
+
|
185
|
+
model = ComplexTestModel.new(id: "complex1", name: "Complex")
|
186
|
+
model.save
|
187
|
+
model.tags.unshift("tag1")
|
188
|
+
model.categories.add("cat1")
|
189
|
+
end
|
190
|
+
result.valid?
|
191
|
+
#=> true
|
192
|
+
|
193
|
+
## Cleanup test environment
|
194
|
+
begin
|
195
|
+
teardown_validation_test
|
196
|
+
rescue => e
|
197
|
+
# Gracefully handle teardown issues during test development
|
198
|
+
puts "Teardown warning: #{e.message}"
|
199
|
+
end
|
200
|
+
|
201
|
+
## Clean up test data
|
202
|
+
test_keys = Familia.dbclient.keys("validationtestmodel:*")
|
203
|
+
test_keys.concat(Familia.dbclient.keys("complextestmodel:*"))
|
204
|
+
test_keys.concat(Familia.dbclient.keys("test:*"))
|
205
|
+
Familia.dbclient.del(*test_keys) if test_keys.any?
|
206
|
+
true
|
207
|
+
#=> true
|