familia 2.0.0.pre15 → 2.0.0.pre17
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/ci.yml +2 -2
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smells.yml +85 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +3 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +54 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +86 -4
- data/CLAUDE.md +39 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +624 -20
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +7 -7
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +51 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +15 -11
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +128 -14
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +12 -171
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +71 -66
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +57 -19
- data/lib/familia/features/object_identifier.rb +134 -25
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
- data/lib/familia/features/relationships/indexing.rb +182 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +10 -7
- data/lib/familia/features.rb +10 -14
- data/lib/familia/field_type.rb +6 -4
- data/lib/familia/horreum/connection.rb +297 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
- data/lib/familia/horreum/serialization.rb +172 -0
- data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
- data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
- data/lib/familia/horreum.rb +222 -119
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +2 -2
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +10 -10
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +11 -10
- data/try/core/errors_try.rb +11 -14
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/familia_try.rb +1 -1
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +3 -3
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/field_groups_try.rb +244 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +443 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +3 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +6 -7
- data/try/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +3 -1
- data/try/horreum/defensive_initialization_try.rb +86 -0
- data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/horreum/settings_try.rb +2 -0
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +2 -2
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +13 -15
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- data/try/valkey.conf +26 -0
- metadata +92 -52
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/horreum/core/connection.rb +0 -73
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -25,14 +25,11 @@ class TestDomain < Familia::Horreum
|
|
25
25
|
field :created_at
|
26
26
|
field :permission_level
|
27
27
|
|
28
|
-
# Basic
|
29
|
-
|
30
|
-
|
28
|
+
# Basic participation with simplified score
|
29
|
+
participates_in TestCustomer, :domains, score: :created_at
|
30
|
+
class_participates_in :all_domains, score: :created_at
|
31
31
|
|
32
32
|
# Note: Indexing features removed for stability
|
33
|
-
|
34
|
-
# Basic membership
|
35
|
-
member_of TestCustomer, :domains
|
36
33
|
end
|
37
34
|
|
38
35
|
class TestTag < Familia::Horreum
|
@@ -42,8 +39,8 @@ class TestTag < Familia::Horreum
|
|
42
39
|
field :name
|
43
40
|
field :created_at
|
44
41
|
|
45
|
-
# Global
|
46
|
-
|
42
|
+
# Global participation
|
43
|
+
class_participates_in :all_tags, score: :created_at
|
47
44
|
end
|
48
45
|
|
49
46
|
# Setup
|
@@ -51,10 +48,10 @@ end
|
|
51
48
|
@domain = TestDomain.new(
|
52
49
|
domain_id: 'dom_789',
|
53
50
|
display_domain: 'example.com',
|
54
|
-
created_at:
|
51
|
+
created_at: Familia.now.to_i,
|
55
52
|
permission_level: :write
|
56
53
|
)
|
57
|
-
@tag = TestTag.new(name: 'important', created_at:
|
54
|
+
@tag = TestTag.new(name: 'important', created_at: Familia.now.to_i)
|
58
55
|
|
59
56
|
# =============================================
|
60
57
|
# 1. V2 Feature Integration Tests
|
@@ -72,10 +69,6 @@ TestDomain.included_modules.map(&:name).include?('Familia::Features::Relationshi
|
|
72
69
|
@domain.respond_to?(:permission_encode)
|
73
70
|
#=> true
|
74
71
|
|
75
|
-
## Redis operations functionality is available
|
76
|
-
@domain.respond_to?(:atomic_operation)
|
77
|
-
#=> true
|
78
|
-
|
79
72
|
## Identifier method works (wraps identifier_field)
|
80
73
|
TestDomain.identifier_field
|
81
74
|
#=> :domain_id
|
@@ -89,7 +82,7 @@ TestDomain.identifier_field
|
|
89
82
|
# =============================================
|
90
83
|
|
91
84
|
## Permission encoding creates proper score
|
92
|
-
@score = @domain.permission_encode(
|
85
|
+
@score = @domain.permission_encode(Familia.now, :write)
|
93
86
|
@score.to_s.match?(/\d+\.\d+/)
|
94
87
|
#=> true
|
95
88
|
|
@@ -99,16 +92,16 @@ decoded[:permission_list].include?(:write)
|
|
99
92
|
#=> true
|
100
93
|
|
101
94
|
## Score encoding preserves timestamp ordering
|
102
|
-
@early_score = @domain.encode_score(
|
103
|
-
@late_score = @domain.encode_score(
|
95
|
+
@early_score = @domain.encode_score(Familia.now - 3600, 100) # 1 hour ago
|
96
|
+
@late_score = @domain.encode_score(Familia.now, 100)
|
104
97
|
@late_score > @early_score
|
105
98
|
#=> true
|
106
99
|
|
107
100
|
# =============================================
|
108
|
-
# 3.
|
101
|
+
# 3. Participation Relationships (participates_in)
|
109
102
|
# =============================================
|
110
103
|
|
111
|
-
## Save operation manages
|
104
|
+
## Save operation manages participation relationships
|
112
105
|
@customer.save
|
113
106
|
@domain.save
|
114
107
|
|
@@ -129,24 +122,24 @@ decoded[:permission_list].include?(:write)
|
|
129
122
|
#=> true
|
130
123
|
|
131
124
|
## Domain can check membership in customer domains (collision-free naming)
|
132
|
-
@domain.respond_to?(:
|
125
|
+
@domain.respond_to?(:in_test_customer_domains?)
|
133
126
|
#=> true
|
134
127
|
|
135
128
|
## Domain can add itself to customer domains (collision-free naming)
|
136
|
-
@domain.respond_to?(:
|
129
|
+
@domain.respond_to?(:add_to_test_customer_domains)
|
137
130
|
#=> true
|
138
131
|
|
139
132
|
## Domain can remove itself from customer domains (collision-free naming)
|
140
|
-
@domain.respond_to?(:
|
133
|
+
@domain.respond_to?(:remove_from_test_customer_domains)
|
141
134
|
#=> true
|
142
135
|
|
143
136
|
## Add domain to customer collection
|
144
|
-
@domain.
|
145
|
-
@domain.
|
137
|
+
@domain.add_to_test_customer_domains(@customer)
|
138
|
+
@domain.in_test_customer_domains?(@customer)
|
146
139
|
#=> true
|
147
140
|
|
148
141
|
## Score is properly encoded
|
149
|
-
score = @domain.
|
142
|
+
score = @domain.score_in_test_customer_domains(@customer)
|
150
143
|
score.is_a?(Float) && score > 0
|
151
144
|
#=> true
|
152
145
|
|
@@ -154,8 +147,8 @@ score.is_a?(Float) && score > 0
|
|
154
147
|
# 4. Basic Functionality Verification
|
155
148
|
# =============================================
|
156
149
|
|
157
|
-
## Domain
|
158
|
-
@domain.respond_to?(:
|
150
|
+
## Domain participation methods work correctly
|
151
|
+
@domain.respond_to?(:score_in_test_customer_domains)
|
159
152
|
#=> true
|
160
153
|
|
161
154
|
## Score calculation methods are available
|
@@ -163,23 +156,23 @@ score.is_a?(Float) && score > 0
|
|
163
156
|
#=> true
|
164
157
|
|
165
158
|
# =============================================
|
166
|
-
# 5.
|
159
|
+
# 5. Bidirectional Participation Methods
|
167
160
|
# =============================================
|
168
161
|
|
169
|
-
##
|
170
|
-
@domain.respond_to?(:
|
162
|
+
## participates_in generates collision-free bidirectional methods with collection names
|
163
|
+
@domain.respond_to?(:add_to_test_customer_domains)
|
171
164
|
#=> true
|
172
165
|
|
173
|
-
## Basic
|
174
|
-
@domain.
|
175
|
-
@domain.
|
166
|
+
## Basic bidirectional participation operations work
|
167
|
+
@domain.remove_from_test_customer_domains(@customer)
|
168
|
+
@domain.in_test_customer_domains?(@customer)
|
176
169
|
#=> false
|
177
170
|
|
178
171
|
# =============================================
|
179
172
|
# 6. Basic Global Tag Tracking Test
|
180
173
|
# =============================================
|
181
174
|
|
182
|
-
## Tag can be
|
175
|
+
## Tag can be participating globally
|
183
176
|
@tag.save
|
184
177
|
@tag.respond_to?(:add_to_class_all_tags)
|
185
178
|
#=> true
|
@@ -213,10 +206,6 @@ temp_key = @domain.create_temp_key("test_operation", 60)
|
|
213
206
|
temp_key.start_with?("temp:")
|
214
207
|
#=> true
|
215
208
|
|
216
|
-
## Batch operations are available
|
217
|
-
@domain.respond_to?(:batch_zadd)
|
218
|
-
#=> true
|
219
|
-
|
220
209
|
## Score range queries work
|
221
210
|
@domain.respond_to?(:score_range)
|
222
211
|
#=> true
|
@@ -17,8 +17,8 @@ Customer.safe_dump_fields
|
|
17
17
|
@cust.custid = 'test@example.com'
|
18
18
|
@cust.role = 'user'
|
19
19
|
@cust.verified = true
|
20
|
-
@cust.created =
|
21
|
-
@cust.updated =
|
20
|
+
@cust.created = Familia.now.to_i
|
21
|
+
@cust.updated = Familia.now.to_i
|
22
22
|
@safe_dump = @cust.safe_dump
|
23
23
|
@safe_dump.keys.sort
|
24
24
|
#=> [:active, :created, :custid, :role, :secrets_created, :updated, :verified]
|
@@ -8,6 +8,8 @@ Familia.debug = false
|
|
8
8
|
Familia.dbclient.flushdb
|
9
9
|
|
10
10
|
class SecretService < Familia::Horreum
|
11
|
+
feature :transient_fields
|
12
|
+
|
11
13
|
identifier_field :name
|
12
14
|
|
13
15
|
field :name
|
@@ -94,7 +96,7 @@ SecretService.fields.sort
|
|
94
96
|
@service.endpoint_url
|
95
97
|
#=> "https://api.example.com"
|
96
98
|
|
97
|
-
##
|
99
|
+
## UnsortedSet transient fields again after refresh
|
98
100
|
@service.api_key = 'new-api-key-after-refresh'
|
99
101
|
@service.password = 'new-password-after-refresh'
|
100
102
|
@service.token = 'new-token-after-refresh'
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# Test cleanup helper for managing anonymous classes and test isolation
|
2
|
+
#
|
3
|
+
# This module provides utilities to create and cleanup test classes that
|
4
|
+
# inherit from Familia classes. Without proper cleanup, anonymous classes
|
5
|
+
# pollute the Familia.members registry causing test failures.
|
6
|
+
|
7
|
+
module TestCleanup
|
8
|
+
@test_classes = []
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_reader :test_classes
|
12
|
+
|
13
|
+
# Create a test class that inherits from the given base class.
|
14
|
+
# The created class is automatically tracked for cleanup.
|
15
|
+
#
|
16
|
+
# @param base_class [Class] The class to inherit from (e.g., Familia::Horreum)
|
17
|
+
# @param block [Proc] Block to define the class body
|
18
|
+
# @return [Class] The created test class
|
19
|
+
def create_test_class(base_class, &block)
|
20
|
+
test_class = Class.new(base_class, &block)
|
21
|
+
track_test_class(test_class)
|
22
|
+
test_class
|
23
|
+
end
|
24
|
+
|
25
|
+
# Track a test class for cleanup. Use this when you create test classes
|
26
|
+
# directly with Class.new instead of using create_test_class.
|
27
|
+
#
|
28
|
+
# @param klass [Class] The test class to track
|
29
|
+
# @return [Class] The tracked class
|
30
|
+
def track_test_class(klass)
|
31
|
+
@test_classes << klass unless @test_classes.include?(klass)
|
32
|
+
klass
|
33
|
+
end
|
34
|
+
|
35
|
+
# Remove all tracked test classes from Familia.members and clear
|
36
|
+
# the tracking array. This should be called in test teardown.
|
37
|
+
#
|
38
|
+
# @return [Array<Class>] The classes that were removed
|
39
|
+
def cleanup_test_classes
|
40
|
+
removed_classes = []
|
41
|
+
|
42
|
+
@test_classes.each do |test_class|
|
43
|
+
if Familia.members.include?(test_class)
|
44
|
+
Familia.unload_member(test_class)
|
45
|
+
removed_classes << test_class
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
@test_classes.clear
|
50
|
+
removed_classes
|
51
|
+
end
|
52
|
+
|
53
|
+
# Clean up all anonymous classes from Familia.members.
|
54
|
+
# This is a more aggressive cleanup that removes any class with nil name.
|
55
|
+
#
|
56
|
+
# @return [Array<Class>] The anonymous classes that were removed
|
57
|
+
def cleanup_anonymous_classes
|
58
|
+
Familia.clear_anonymous_members
|
59
|
+
end
|
60
|
+
|
61
|
+
# Perform complete test cleanup - both tracked and anonymous classes
|
62
|
+
#
|
63
|
+
# @return [Hash] Summary of cleanup performed
|
64
|
+
def complete_cleanup
|
65
|
+
tracked_removed = cleanup_test_classes
|
66
|
+
anonymous_removed = cleanup_anonymous_classes
|
67
|
+
|
68
|
+
{
|
69
|
+
tracked_classes_removed: tracked_removed.size,
|
70
|
+
anonymous_classes_removed: anonymous_removed.size,
|
71
|
+
total_removed: tracked_removed.size + anonymous_removed.size
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Automatically perform cleanup if we're in test mode
|
78
|
+
# This ensures cleanup happens even if tests don't explicitly call it
|
79
|
+
at_exit do
|
80
|
+
if Familia.test_mode? && TestCleanup.test_classes.any?
|
81
|
+
cleanup_result = TestCleanup.complete_cleanup
|
82
|
+
if cleanup_result[:total_removed] > 0
|
83
|
+
puts "[TestCleanup] Cleaned up #{cleanup_result[:total_removed]} test classes on exit"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/try/helpers/test_helpers.rb
CHANGED
@@ -10,6 +10,7 @@ require_relative '../../lib/familia'
|
|
10
10
|
|
11
11
|
Familia.enable_database_logging = true
|
12
12
|
Familia.enable_database_counter = true
|
13
|
+
Familia.uri = 'redis://127.0.0.1:2525'
|
13
14
|
|
14
15
|
class Bone < Familia::Horreum
|
15
16
|
using Familia::Refinements::TimeLiterals
|
@@ -44,12 +45,10 @@ class Customer < Familia::Horreum
|
|
44
45
|
|
45
46
|
using Familia::Refinements::TimeLiterals
|
46
47
|
|
47
|
-
logical_database
|
48
|
+
logical_database 3 # Use something other than the default DB
|
48
49
|
default_expiration 5.years
|
49
50
|
|
50
51
|
feature :safe_dump
|
51
|
-
# feature :expiration
|
52
|
-
# feature :api_version
|
53
52
|
|
54
53
|
# Use new SafeDump DSL instead of @safe_dump_fields
|
55
54
|
safe_dump_field :custid
|
@@ -65,7 +64,7 @@ class Customer < Familia::Horreum
|
|
65
64
|
# We use a callable here since `:active?` is not a valid method symbol.
|
66
65
|
safe_dump_field :active, ->(cust) { cust.active? }
|
67
66
|
|
68
|
-
class_sorted_set :values, key: '
|
67
|
+
class_sorted_set :values, key: 'familia:customer'
|
69
68
|
class_hashkey :domains
|
70
69
|
|
71
70
|
hashkey :stripe_customer
|
@@ -91,9 +90,9 @@ class Customer < Familia::Horreum
|
|
91
90
|
field :reset_requested #=> Boolean
|
92
91
|
|
93
92
|
hashkey :password_reset #=> Familia::HashKey
|
94
|
-
list :sessions #=> Familia::
|
93
|
+
list :sessions #=> Familia::ListKey
|
95
94
|
|
96
|
-
class_list :
|
95
|
+
class_list :all_customers, suffix: [] # no suffix
|
97
96
|
class_string :message
|
98
97
|
|
99
98
|
class_zset :instances, class: self, reference: true
|
@@ -108,7 +107,7 @@ end
|
|
108
107
|
class Session < Familia::Horreum
|
109
108
|
using Familia::Refinements::TimeLiterals
|
110
109
|
|
111
|
-
logical_database
|
110
|
+
logical_database 2 # a non-default database
|
112
111
|
default_expiration 180.minutes
|
113
112
|
|
114
113
|
identifier_field :sessid
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# try/horreum/auto_indexing_on_save_try.rb
|
2
|
+
|
3
|
+
#
|
4
|
+
# Auto-indexing on save functionality tests
|
5
|
+
# Tests automatic index population when Familia::Horreum objects are saved
|
6
|
+
#
|
7
|
+
|
8
|
+
require_relative '../helpers/test_helpers'
|
9
|
+
|
10
|
+
# Test classes for auto-indexing functionality
|
11
|
+
class ::AutoIndexUser < Familia::Horreum
|
12
|
+
feature :relationships
|
13
|
+
|
14
|
+
identifier_field :user_id
|
15
|
+
field :user_id
|
16
|
+
field :email
|
17
|
+
field :username
|
18
|
+
field :department
|
19
|
+
|
20
|
+
# Class-level unique indexes (should auto-populate on save)
|
21
|
+
unique_index :email, :email_index
|
22
|
+
unique_index :username, :username_index
|
23
|
+
end
|
24
|
+
|
25
|
+
class ::AutoIndexCompany < Familia::Horreum
|
26
|
+
feature :relationships
|
27
|
+
|
28
|
+
identifier_field :company_id
|
29
|
+
field :company_id
|
30
|
+
field :name
|
31
|
+
end
|
32
|
+
|
33
|
+
class ::AutoIndexEmployee < Familia::Horreum
|
34
|
+
feature :relationships
|
35
|
+
|
36
|
+
identifier_field :emp_id
|
37
|
+
field :emp_id
|
38
|
+
field :badge_number
|
39
|
+
field :department
|
40
|
+
|
41
|
+
# Instance-scoped indexes (should NOT auto-populate - require parent context)
|
42
|
+
unique_index :badge_number, :badge_index, within: AutoIndexCompany
|
43
|
+
multi_index :department, :dept_index, within: AutoIndexCompany
|
44
|
+
end
|
45
|
+
|
46
|
+
# Setup
|
47
|
+
@user_id = "user_#{rand(1000000)}"
|
48
|
+
@user = AutoIndexUser.new(user_id: @user_id, email: 'test@example.com', username: 'testuser', department: 'engineering')
|
49
|
+
|
50
|
+
@company_id = "comp_#{rand(1000000)}"
|
51
|
+
@company = AutoIndexCompany.new(company_id: @company_id, name: 'Test Corp')
|
52
|
+
|
53
|
+
@emp_id = "emp_#{rand(1000000)}"
|
54
|
+
@employee = AutoIndexEmployee.new(emp_id: @emp_id, badge_number: 'BADGE123', department: 'sales')
|
55
|
+
|
56
|
+
# =============================================
|
57
|
+
# 1. Class-Level Unique Index Auto-Population
|
58
|
+
# =============================================
|
59
|
+
|
60
|
+
## Unique index is empty before save
|
61
|
+
AutoIndexUser.email_index.has_key?('test@example.com')
|
62
|
+
#=> false
|
63
|
+
|
64
|
+
## Save automatically populates unique index
|
65
|
+
@user.save
|
66
|
+
AutoIndexUser.email_index.has_key?('test@example.com')
|
67
|
+
#=> true
|
68
|
+
|
69
|
+
## Auto-populated index maps to correct identifier
|
70
|
+
AutoIndexUser.email_index.get('test@example.com')
|
71
|
+
#=> @user_id
|
72
|
+
|
73
|
+
## Finder method works after auto-indexing
|
74
|
+
found = AutoIndexUser.find_by_email('test@example.com')
|
75
|
+
found&.user_id
|
76
|
+
#=> @user_id
|
77
|
+
|
78
|
+
## Multiple unique indexes auto-populate on same save
|
79
|
+
AutoIndexUser.username_index.get('testuser')
|
80
|
+
#=> @user_id
|
81
|
+
|
82
|
+
## Subsequent saves maintain index (idempotent)
|
83
|
+
@user.save
|
84
|
+
AutoIndexUser.email_index.get('test@example.com')
|
85
|
+
#=> @user_id
|
86
|
+
|
87
|
+
## Changing indexed field and saving adds new entry (old entry remains unless manually removed)
|
88
|
+
# Note: Auto-indexing is idempotent addition only - updates require manual update_in_class_* calls
|
89
|
+
@user.email = 'newemail@example.com'
|
90
|
+
@user.save
|
91
|
+
# New email is indexed, but old email remains (expected behavior - use update_in_class_* for proper updates)
|
92
|
+
[AutoIndexUser.email_index.has_key?('test@example.com'), AutoIndexUser.email_index.get('newemail@example.com') == @user_id]
|
93
|
+
#=> [true, true]
|
94
|
+
|
95
|
+
# =============================================
|
96
|
+
# 2. Instance-Scoped Indexes (Manual Only)
|
97
|
+
# =============================================
|
98
|
+
|
99
|
+
## Instance-scoped indexes do NOT auto-populate on save
|
100
|
+
@employee.save
|
101
|
+
@company.badge_index.has_key?('BADGE123')
|
102
|
+
#=> false
|
103
|
+
|
104
|
+
## Instance-scoped indexes remain manual (require parent context)
|
105
|
+
@employee.add_to_auto_index_company_badge_index(@company)
|
106
|
+
@company.badge_index.has_key?('BADGE123')
|
107
|
+
#=> true
|
108
|
+
|
109
|
+
# =============================================
|
110
|
+
# 3. Edge Cases and Error Handling
|
111
|
+
# =============================================
|
112
|
+
|
113
|
+
## Nil field values handled gracefully
|
114
|
+
@user_nil_id = "user_nil_#{rand(1000000)}"
|
115
|
+
@user_nil = AutoIndexUser.new(user_id: @user_nil_id, email: nil, username: nil, department: nil)
|
116
|
+
@user_nil.save
|
117
|
+
AutoIndexUser.email_index.has_key?('')
|
118
|
+
#=> false
|
119
|
+
|
120
|
+
## Empty string field values handled gracefully
|
121
|
+
@user_empty_id = "user_empty_#{rand(1000000)}"
|
122
|
+
@user_empty = AutoIndexUser.new(user_id: @user_empty_id, email: '', username: '', department: '')
|
123
|
+
@user_empty.save
|
124
|
+
# Empty strings are indexed (they're valid string values, just empty)
|
125
|
+
AutoIndexUser.email_index.has_key?('')
|
126
|
+
#=> true
|
127
|
+
|
128
|
+
## Auto-indexing works with create method
|
129
|
+
@user2_id = "user_#{rand(1000000)}"
|
130
|
+
@user2 = AutoIndexUser.create(user_id: @user2_id, email: 'create@example.com', username: 'createuser', department: 'marketing')
|
131
|
+
AutoIndexUser.find_by_email('create@example.com')&.user_id
|
132
|
+
#=> @user2_id
|
133
|
+
|
134
|
+
## Auto-indexing idempotent with multiple saves
|
135
|
+
@user2.save
|
136
|
+
@user2.save
|
137
|
+
@user2.save
|
138
|
+
AutoIndexUser.email_index.get('create@example.com')
|
139
|
+
#=> @user2_id
|
140
|
+
|
141
|
+
## Field update followed by save adds new entry (use update_in_class_* for proper updates)
|
142
|
+
old_email = @user2.email
|
143
|
+
@user2.email = 'updated@example.com'
|
144
|
+
@user2.save
|
145
|
+
# Both old and new emails are indexed (auto-indexing doesn't remove old values)
|
146
|
+
# For proper updates that remove old values, use: @user2.update_in_class_email_index(old_email)
|
147
|
+
[AutoIndexUser.email_index.has_key?(old_email), AutoIndexUser.email_index.get('updated@example.com') == @user2_id]
|
148
|
+
#=> [true, true]
|
149
|
+
|
150
|
+
# =============================================
|
151
|
+
# 4. Integration with Other Features
|
152
|
+
# =============================================
|
153
|
+
|
154
|
+
## Auto-indexing works with transient fields
|
155
|
+
class ::AutoIndexWithTransient < Familia::Horreum
|
156
|
+
feature :transient_fields
|
157
|
+
feature :relationships
|
158
|
+
|
159
|
+
identifier_field :id
|
160
|
+
field :id
|
161
|
+
field :email
|
162
|
+
transient_field :temp_value
|
163
|
+
|
164
|
+
unique_index :email, :email_index
|
165
|
+
end
|
166
|
+
|
167
|
+
@transient_id = "trans_#{rand(1000000)}"
|
168
|
+
@transient_obj = AutoIndexWithTransient.new(id: @transient_id, email: 'transient@example.com', temp_value: 'ignored')
|
169
|
+
@transient_obj.save
|
170
|
+
AutoIndexWithTransient.find_by_email('transient@example.com')&.id
|
171
|
+
#=> @transient_id
|
172
|
+
|
173
|
+
## Auto-indexing works regardless of other features
|
174
|
+
# Just verify that the feature system doesn't interfere
|
175
|
+
@transient_obj.class.respond_to?(:indexing_relationships)
|
176
|
+
#=> true
|
177
|
+
|
178
|
+
# =============================================
|
179
|
+
# 5. Performance and Behavior Verification
|
180
|
+
# =============================================
|
181
|
+
|
182
|
+
## Auto-indexing has negligible overhead (no existence checks)
|
183
|
+
# This test verifies the design: we use idempotent commands (HSET, SADD)
|
184
|
+
# rather than checking if the index exists before updating
|
185
|
+
@user4_id = "user_#{rand(1000000)}"
|
186
|
+
@user4 = AutoIndexUser.new(user_id: @user4_id, email: 'perf@example.com', username: 'perfuser', department: 'ops')
|
187
|
+
|
188
|
+
# Save multiple times - all should succeed with same result
|
189
|
+
@user4.save
|
190
|
+
@user4.save
|
191
|
+
@user4.save
|
192
|
+
|
193
|
+
AutoIndexUser.email_index.get('perf@example.com')
|
194
|
+
#=> @user4_id
|
195
|
+
|
196
|
+
## Auto-indexing only processes class-level indexes
|
197
|
+
# Verify no errors when instance-scoped indexes present
|
198
|
+
@employee2_id = "emp_#{rand(1000000)}"
|
199
|
+
@employee2 = AutoIndexEmployee.new(emp_id: @employee2_id, badge_number: 'BADGE456', department: 'engineering')
|
200
|
+
@employee2.save # Should not error, just skip instance-scoped indexes
|
201
|
+
@employee2.emp_id
|
202
|
+
#=> @employee2_id
|
203
|
+
|
204
|
+
# Teardown - clean up test objects
|
205
|
+
[@user, @user2, @user4, @user_nil, @user_empty, @company, @employee, @employee2, @transient_obj].each do |obj|
|
206
|
+
obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
|
207
|
+
end
|
208
|
+
|
209
|
+
# Clean up class-level indexes
|
210
|
+
[AutoIndexUser.email_index, AutoIndexUser.username_index].each do |index|
|
211
|
+
index.delete! if index.respond_to?(:delete!) && index.respond_to?(:exists?) && index.exists?
|
212
|
+
end
|
data/try/horreum/base_try.rb
CHANGED
@@ -52,7 +52,7 @@ Familia.debug = false
|
|
52
52
|
#=> true
|
53
53
|
|
54
54
|
## Horreum object fields have a fast attribute method (1 of 2)
|
55
|
-
Familia.trace :LOAD,
|
55
|
+
Familia.trace :LOAD, nil, @customer.uri if Familia.debug?
|
56
56
|
@customer.name! 'Jane Doe'
|
57
57
|
#=> true
|
58
58
|
|
@@ -69,7 +69,8 @@ Familia.trace :LOAD, @customer.dbclient, @customer.uri, caller if Familia.debug?
|
|
69
69
|
|
70
70
|
## Horreum objects can be destroyed
|
71
71
|
@customer.destroy!
|
72
|
-
|
72
|
+
#=:> MultiResult
|
73
|
+
#==> result.successful?
|
73
74
|
|
74
75
|
## All horrerum objects have a key field
|
75
76
|
@customer.identifier
|
data/try/horreum/commands_try.rb
CHANGED
@@ -0,0 +1,86 @@
|
|
1
|
+
# try/horreum/defensive_initialization_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
# Test defensive initialization behavior
|
6
|
+
class User < Familia::Horreum
|
7
|
+
field :email
|
8
|
+
list :sessions
|
9
|
+
zset :metrics
|
10
|
+
|
11
|
+
def initialize(email = nil)
|
12
|
+
# This is the common mistake - overriding initialize without calling super
|
13
|
+
@email = email
|
14
|
+
# Missing: super() or initialize_relatives
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class SafeUser < Familia::Horreum
|
19
|
+
field :email
|
20
|
+
list :sessions
|
21
|
+
zset :metrics
|
22
|
+
|
23
|
+
def init
|
24
|
+
# This is the correct way - using the init hook
|
25
|
+
# Fields are already set by initialize, no need to override
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Setup instances for testing
|
30
|
+
@user = User.new("test@example.com")
|
31
|
+
@safe_user = SafeUser.new
|
32
|
+
@safe_user.email = "safe@example.com"
|
33
|
+
|
34
|
+
## Test that accessing relationships after bad initialize triggers lazy initialization
|
35
|
+
@user.email
|
36
|
+
#=> "test@example.com"
|
37
|
+
|
38
|
+
## Test that sessions works with lazy initialization
|
39
|
+
@user.sessions.class
|
40
|
+
#=> Familia::ListKey
|
41
|
+
|
42
|
+
## Test that metrics also works with lazy initialization
|
43
|
+
@user.metrics.class
|
44
|
+
#=> Familia::SortedSet
|
45
|
+
|
46
|
+
## Test that safe user works normally
|
47
|
+
@safe_user.email
|
48
|
+
#=> "safe@example.com"
|
49
|
+
|
50
|
+
## Test that safe user sessions work
|
51
|
+
@safe_user.sessions.class
|
52
|
+
#=> Familia::ListKey
|
53
|
+
|
54
|
+
## Test that relatives_initialized flag prevents double initialization
|
55
|
+
@user.singleton_class.instance_variable_get(:@relatives_initialized)
|
56
|
+
#=> true
|
57
|
+
|
58
|
+
## Test that manual initialize_relatives call is no-op
|
59
|
+
@user.initialize_relatives
|
60
|
+
@user.sessions.class
|
61
|
+
#=> Familia::ListKey
|
62
|
+
|
63
|
+
## Test that the original problem is now fixed - bad override still works
|
64
|
+
class BadUser < Familia::Horreum
|
65
|
+
field :email
|
66
|
+
list :sessions
|
67
|
+
|
68
|
+
def initialize(email)
|
69
|
+
# Bad: overriding initialize without calling super
|
70
|
+
@email = email
|
71
|
+
# Missing: super() or initialize_relatives
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
@bad_user = BadUser.new("bad@example.com")
|
76
|
+
@bad_user.email
|
77
|
+
#=> "bad@example.com"
|
78
|
+
|
79
|
+
## Test that relationships work despite bad initialize (lazy initialization kicks in)
|
80
|
+
@bad_user.sessions.class
|
81
|
+
#=> Familia::ListKey
|
82
|
+
|
83
|
+
## Test that the bad user can actually use the relationships
|
84
|
+
@bad_user.sessions.add("session_123")
|
85
|
+
@bad_user.sessions.size > 0
|
86
|
+
#=> true
|