familia 2.0.0.pre25 → 2.0.0
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 +1 -0
- data/CHANGELOG.rst +69 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +2 -2
- data/README.md +1 -3
- data/docs/guides/feature-encrypted-fields.md +1 -1
- data/docs/guides/feature-expiration.md +1 -1
- data/docs/guides/feature-quantization.md +1 -1
- data/docs/overview.md +7 -7
- data/docs/reference/api-technical.md +103 -7
- data/familia.gemspec +1 -2
- data/lib/familia/data_type/types/hashkey.rb +238 -0
- data/lib/familia/data_type/types/listkey.rb +110 -4
- data/lib/familia/data_type/types/sorted_set.rb +365 -0
- data/lib/familia/data_type/types/stringkey.rb +139 -0
- data/lib/familia/data_type/types/unsorted_set.rb +122 -2
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +2 -1
- data/lib/familia/features/relationships/participation/through_model_operations.rb +4 -3
- data/lib/familia/features/relationships/participation.rb +6 -6
- data/lib/familia/horreum/management.rb +29 -0
- data/lib/familia/version.rb +1 -1
- data/try/features/relationships/prefix_vs_config_name_try.rb +418 -0
- metadata +3 -27
- data/docs/migrating/v2.0.0-pre.md +0 -84
- data/docs/migrating/v2.0.0-pre11.md +0 -253
- data/docs/migrating/v2.0.0-pre12.md +0 -306
- data/docs/migrating/v2.0.0-pre13.md +0 -95
- data/docs/migrating/v2.0.0-pre14.md +0 -37
- data/docs/migrating/v2.0.0-pre18.md +0 -58
- data/docs/migrating/v2.0.0-pre19.md +0 -197
- data/docs/migrating/v2.0.0-pre22.md +0 -241
- data/docs/migrating/v2.0.0-pre5.md +0 -131
- data/docs/migrating/v2.0.0-pre6.md +0 -154
- data/docs/migrating/v2.0.0-pre7.md +0 -222
|
@@ -87,9 +87,113 @@ module Familia
|
|
|
87
87
|
end
|
|
88
88
|
alias remove remove_element # deprecated
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
# Returns the intersection of this set with one or more other sets.
|
|
91
|
+
# @param other_sets [Array<UnsortedSet, String>] Other sets (as UnsortedSet instances or raw keys)
|
|
92
|
+
# @return [Array] Deserialized members present in all sets
|
|
93
|
+
def intersection(*other_sets)
|
|
94
|
+
keys = extract_keys(other_sets)
|
|
95
|
+
elements = dbclient.sinter(dbkey, *keys)
|
|
96
|
+
deserialize_values(*elements)
|
|
97
|
+
end
|
|
98
|
+
alias inter intersection
|
|
99
|
+
|
|
100
|
+
# Returns the union of this set with one or more other sets.
|
|
101
|
+
# @param other_sets [Array<UnsortedSet, String>] Other sets (as UnsortedSet instances or raw keys)
|
|
102
|
+
# @return [Array] Deserialized members present in any of the sets
|
|
103
|
+
def union(*other_sets)
|
|
104
|
+
keys = extract_keys(other_sets)
|
|
105
|
+
elements = dbclient.sunion(dbkey, *keys)
|
|
106
|
+
deserialize_values(*elements)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns the difference of this set minus one or more other sets.
|
|
110
|
+
# @param other_sets [Array<UnsortedSet, String>] Other sets (as UnsortedSet instances or raw keys)
|
|
111
|
+
# @return [Array] Deserialized members present in this set but not in any other sets
|
|
112
|
+
def difference(*other_sets)
|
|
113
|
+
keys = extract_keys(other_sets)
|
|
114
|
+
elements = dbclient.sdiff(dbkey, *keys)
|
|
115
|
+
deserialize_values(*elements)
|
|
116
|
+
end
|
|
117
|
+
alias diff difference
|
|
118
|
+
|
|
119
|
+
# Checks membership for multiple values at once.
|
|
120
|
+
# @param values [Array] Values to check for membership
|
|
121
|
+
# @return [Array<Boolean>] Array of booleans indicating membership for each value
|
|
122
|
+
def member_any?(*values)
|
|
123
|
+
values = values.flatten
|
|
124
|
+
serialized = values.map { |v| serialize_value(v) }
|
|
125
|
+
dbclient.smismember(dbkey, *serialized)
|
|
126
|
+
end
|
|
127
|
+
alias members? member_any?
|
|
128
|
+
|
|
129
|
+
# Iterates over set members using cursor-based iteration.
|
|
130
|
+
# @param cursor [Integer] Starting cursor position (default: 0)
|
|
131
|
+
# @param match [String, nil] Optional pattern to filter members
|
|
132
|
+
# @param count [Integer, nil] Optional hint for number of elements to return per call
|
|
133
|
+
# @return [Array<Integer, Array>] Two-element array: [new_cursor, deserialized_members]
|
|
134
|
+
def scan(cursor = 0, match: nil, count: nil)
|
|
135
|
+
opts = {}
|
|
136
|
+
opts[:match] = match if match
|
|
137
|
+
opts[:count] = count if count
|
|
138
|
+
|
|
139
|
+
new_cursor, elements = dbclient.sscan(dbkey, cursor, **opts)
|
|
140
|
+
[new_cursor.to_i, deserialize_values(*elements)]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Returns the cardinality of the intersection without retrieving members.
|
|
144
|
+
# More memory-efficient than intersection when only the count is needed.
|
|
145
|
+
# @param other_sets [Array<UnsortedSet, String>] Other sets (as UnsortedSet instances or raw keys)
|
|
146
|
+
# @param limit [Integer] Stop counting after reaching this limit (0 = no limit)
|
|
147
|
+
# @return [Integer] Number of elements in the intersection
|
|
148
|
+
def intercard(*other_sets, limit: 0)
|
|
149
|
+
keys = extract_keys(other_sets)
|
|
150
|
+
all_keys = [dbkey, *keys]
|
|
151
|
+
if limit.positive?
|
|
152
|
+
dbclient.sintercard(all_keys.size, *all_keys, limit: limit)
|
|
153
|
+
else
|
|
154
|
+
dbclient.sintercard(all_keys.size, *all_keys)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
alias intersection_cardinality intercard
|
|
158
|
+
|
|
159
|
+
# Stores the intersection of this set with other sets into a destination key.
|
|
160
|
+
# @param destination [UnsortedSet, String] Destination set (as UnsortedSet instance or raw key)
|
|
161
|
+
# @param other_sets [Array<UnsortedSet, String>] Other sets to intersect with
|
|
162
|
+
# @return [Integer] Number of elements in the resulting set
|
|
163
|
+
def interstore(destination, *other_sets)
|
|
164
|
+
dest_key = extract_key(destination)
|
|
165
|
+
keys = extract_keys(other_sets)
|
|
166
|
+
result = dbclient.sinterstore(dest_key, dbkey, *keys)
|
|
167
|
+
update_expiration
|
|
168
|
+
result
|
|
169
|
+
end
|
|
170
|
+
alias intersection_store interstore
|
|
171
|
+
|
|
172
|
+
# Stores the union of this set with other sets into a destination key.
|
|
173
|
+
# @param destination [UnsortedSet, String] Destination set (as UnsortedSet instance or raw key)
|
|
174
|
+
# @param other_sets [Array<UnsortedSet, String>] Other sets to union with
|
|
175
|
+
# @return [Integer] Number of elements in the resulting set
|
|
176
|
+
def unionstore(destination, *other_sets)
|
|
177
|
+
dest_key = extract_key(destination)
|
|
178
|
+
keys = extract_keys(other_sets)
|
|
179
|
+
result = dbclient.sunionstore(dest_key, dbkey, *keys)
|
|
180
|
+
update_expiration
|
|
181
|
+
result
|
|
182
|
+
end
|
|
183
|
+
alias union_store unionstore
|
|
184
|
+
|
|
185
|
+
# Stores the difference of this set minus other sets into a destination key.
|
|
186
|
+
# @param destination [UnsortedSet, String] Destination set (as UnsortedSet instance or raw key)
|
|
187
|
+
# @param other_sets [Array<UnsortedSet, String>] Other sets to subtract
|
|
188
|
+
# @return [Integer] Number of elements in the resulting set
|
|
189
|
+
def diffstore(destination, *other_sets)
|
|
190
|
+
dest_key = extract_key(destination)
|
|
191
|
+
keys = extract_keys(other_sets)
|
|
192
|
+
result = dbclient.sdiffstore(dest_key, dbkey, *keys)
|
|
193
|
+
update_expiration
|
|
194
|
+
result
|
|
92
195
|
end
|
|
196
|
+
alias difference_store diffstore
|
|
93
197
|
|
|
94
198
|
def pop
|
|
95
199
|
dbclient.spop dbkey
|
|
@@ -115,6 +219,22 @@ module Familia
|
|
|
115
219
|
end
|
|
116
220
|
alias random sampleraw
|
|
117
221
|
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
# Extracts the database key from a set reference.
|
|
225
|
+
# @param set_ref [UnsortedSet, String] An UnsortedSet instance or raw key string
|
|
226
|
+
# @return [String] The database key
|
|
227
|
+
def extract_key(set_ref)
|
|
228
|
+
set_ref.respond_to?(:dbkey) ? set_ref.dbkey : set_ref.to_s
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Extracts database keys from an array of set references.
|
|
232
|
+
# @param set_refs [Array<UnsortedSet, String>] Array of UnsortedSet instances or raw keys
|
|
233
|
+
# @return [Array<String>] Array of database keys
|
|
234
|
+
def extract_keys(set_refs)
|
|
235
|
+
set_refs.flatten.map { |s| extract_key(s) }
|
|
236
|
+
end
|
|
237
|
+
|
|
118
238
|
Familia::DataType.register self, :set
|
|
119
239
|
Familia::DataType.register self, :unsorted_set
|
|
120
240
|
end
|
|
@@ -301,7 +301,8 @@ module Familia
|
|
|
301
301
|
# Build key pattern for SCAN
|
|
302
302
|
# For instance-scoped indexes, we still scan all objects of indexed_class
|
|
303
303
|
# (not scoped under parent), then filter by scope during processing
|
|
304
|
-
|
|
304
|
+
# Use centralized scan_pattern method for consistent key generation
|
|
305
|
+
pattern = indexed_class.scan_pattern
|
|
305
306
|
|
|
306
307
|
Familia.info "[Rebuild] Starting via_scan for #{indexed_class.name}.#{field} (pattern: #{pattern})"
|
|
307
308
|
Familia.warn "[Rebuild] Using SCAN fallback - consider adding instances collection for better performance"
|
|
@@ -51,9 +51,10 @@ module Familia
|
|
|
51
51
|
# @return [String] Deterministic key for the through model
|
|
52
52
|
#
|
|
53
53
|
def build_key(target:, participant:, through_class:)
|
|
54
|
-
|
|
55
|
-
"#{
|
|
56
|
-
"#{
|
|
54
|
+
# Use prefix for Redis key construction (prefix may differ from config_name if explicitly set)
|
|
55
|
+
"#{target.class.prefix}:#{target.objid}:" \
|
|
56
|
+
"#{participant.class.prefix}:#{participant.objid}:" \
|
|
57
|
+
"#{through_class.prefix}"
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
# Find or create a through model instance
|
|
@@ -600,9 +600,8 @@ module Familia
|
|
|
600
600
|
# @param collection_names [Array<String>, nil] Optional collection name filter
|
|
601
601
|
# @return [Array<String>] Array of unique target instance IDs
|
|
602
602
|
def participating_ids_for_target(target_class, collection_names = nil)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
target_prefix = "#{target_class.config_name}#{Familia.delim}"
|
|
603
|
+
# Use centralized key_prefix method for consistent key generation
|
|
604
|
+
target_prefix = target_class.key_prefix
|
|
606
605
|
ids = Set.new
|
|
607
606
|
|
|
608
607
|
participations.members.each do |key|
|
|
@@ -635,7 +634,8 @@ module Familia
|
|
|
635
634
|
# @param collection_names [Array<String>, nil] Optional collection name filter
|
|
636
635
|
# @return [Boolean] true if any matching participation exists
|
|
637
636
|
def participating_in_target?(target_class, collection_names = nil)
|
|
638
|
-
|
|
637
|
+
# Use centralized key_prefix method for consistent key generation
|
|
638
|
+
target_prefix = target_class.key_prefix
|
|
639
639
|
|
|
640
640
|
participations.members.any? do |key|
|
|
641
641
|
next false unless key.start_with?(target_prefix)
|
|
@@ -667,9 +667,9 @@ module Familia
|
|
|
667
667
|
next unless target_class_config && target_id && collection_name_from_key
|
|
668
668
|
|
|
669
669
|
# Find the matching participation configuration
|
|
670
|
-
# Note: target_class_config from key
|
|
670
|
+
# Note: target_class_config from key uses prefix (may differ from config_name)
|
|
671
671
|
config = self.class.participation_relationships.find do |cfg|
|
|
672
|
-
cfg.target_class.
|
|
672
|
+
cfg.target_class.prefix.to_s == target_class_config &&
|
|
673
673
|
cfg.collection_name.to_s == collection_name_from_key
|
|
674
674
|
end
|
|
675
675
|
|
|
@@ -471,6 +471,35 @@ module Familia
|
|
|
471
471
|
# at the class level are created without the global default 'object'
|
|
472
472
|
# suffix. See DataType#dbkey "parent_class?" for more details.
|
|
473
473
|
#
|
|
474
|
+
|
|
475
|
+
# Returns the key prefix for this class including the delimiter.
|
|
476
|
+
#
|
|
477
|
+
# Centralizes key prefix generation to prevent bugs from manual
|
|
478
|
+
# string interpolation across the codebase.
|
|
479
|
+
#
|
|
480
|
+
# @return [String] The prefix with delimiter (e.g., "customer:")
|
|
481
|
+
# @example
|
|
482
|
+
# User.key_prefix #=> "user:"
|
|
483
|
+
#
|
|
484
|
+
def key_prefix
|
|
485
|
+
"#{prefix}#{Familia.delim}"
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Returns the SCAN pattern for finding all objects of this class.
|
|
489
|
+
#
|
|
490
|
+
# Centralizes SCAN pattern generation to ensure consistency across
|
|
491
|
+
# rebuild strategies and other key enumeration operations.
|
|
492
|
+
#
|
|
493
|
+
# @param match_suffix [String] The suffix to match (default: class suffix)
|
|
494
|
+
# @return [String] The SCAN pattern (e.g., "customer:*:object")
|
|
495
|
+
# @example
|
|
496
|
+
# User.scan_pattern #=> "user:*:object"
|
|
497
|
+
# User.scan_pattern('active') #=> "user:*:active"
|
|
498
|
+
#
|
|
499
|
+
def scan_pattern(match_suffix = suffix)
|
|
500
|
+
"#{prefix}:*:#{match_suffix}"
|
|
501
|
+
end
|
|
502
|
+
|
|
474
503
|
def dbkey(identifier, suffix = self.suffix)
|
|
475
504
|
if identifier.to_s.empty?
|
|
476
505
|
raise NoIdentifier, "#{self} requires non-empty identifier, got: #{identifier.inspect}"
|
data/lib/familia/version.rb
CHANGED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# try/features/relationships/prefix_vs_config_name_try.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Tests for the bug fix where reverse lookup methods now correctly use `prefix`
|
|
6
|
+
# instead of `config_name` for Redis key matching.
|
|
7
|
+
#
|
|
8
|
+
# Background: When a class declares an explicit `prefix` that differs from its
|
|
9
|
+
# computed `config_name`, reverse lookups (like *_instances, *_ids) would fail
|
|
10
|
+
# to find the correct keys because they were matching against `config_name`
|
|
11
|
+
# instead of `prefix`.
|
|
12
|
+
#
|
|
13
|
+
# Example: CustomDomain with `prefix :customdomain` (no underscore) vs
|
|
14
|
+
# `config_name` returning "custom_domain" (with underscore)
|
|
15
|
+
|
|
16
|
+
require_relative '../../support/helpers/test_helpers'
|
|
17
|
+
|
|
18
|
+
Familia.debug = false
|
|
19
|
+
|
|
20
|
+
# Scenario 1: Default prefix (no explicit prefix set)
|
|
21
|
+
# prefix should equal config_name.to_sym
|
|
22
|
+
class ::PrefixTestSimpleModel < Familia::Horreum
|
|
23
|
+
feature :relationships
|
|
24
|
+
|
|
25
|
+
identifier_field :model_id
|
|
26
|
+
field :model_id
|
|
27
|
+
field :name
|
|
28
|
+
|
|
29
|
+
sorted_set :participants
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class ::PrefixTestSimpleParticipant < Familia::Horreum
|
|
33
|
+
feature :relationships
|
|
34
|
+
|
|
35
|
+
identifier_field :participant_id
|
|
36
|
+
field :participant_id
|
|
37
|
+
field :created_at
|
|
38
|
+
|
|
39
|
+
participates_in PrefixTestSimpleModel, :participants, score: :created_at
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Scenario 2: Explicit prefix matching config_name
|
|
43
|
+
# Should work exactly the same as default
|
|
44
|
+
class ::PrefixMatchingTeam < Familia::Horreum
|
|
45
|
+
feature :relationships
|
|
46
|
+
prefix :prefix_matching_team # Matches config_name
|
|
47
|
+
|
|
48
|
+
identifier_field :team_id
|
|
49
|
+
field :team_id
|
|
50
|
+
field :name
|
|
51
|
+
|
|
52
|
+
sorted_set :members
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class ::PrefixMatchingMember < Familia::Horreum
|
|
56
|
+
feature :relationships
|
|
57
|
+
|
|
58
|
+
identifier_field :member_id
|
|
59
|
+
field :member_id
|
|
60
|
+
field :joined_at
|
|
61
|
+
|
|
62
|
+
participates_in PrefixMatchingTeam, :members, score: :joined_at
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Scenario 3: THE BUG CASE - Explicit prefix differs from config_name
|
|
66
|
+
# CustomDomain: config_name = "custom_domain", prefix = :customdomain
|
|
67
|
+
class ::PrefixMismatchedDomain < Familia::Horreum
|
|
68
|
+
feature :relationships
|
|
69
|
+
prefix :mismatcheddomain # No underscore - differs from config_name "prefix_mismatched_domain"
|
|
70
|
+
|
|
71
|
+
identifier_field :domain_id
|
|
72
|
+
field :domain_id
|
|
73
|
+
field :display_domain
|
|
74
|
+
field :created_at
|
|
75
|
+
|
|
76
|
+
participates_in PrefixTestSimpleModel, :participants, score: :created_at
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Scenario 4: Another mismatched prefix case - APIKey pattern
|
|
80
|
+
# APIKey: config_name = "api_key", prefix = :apikey
|
|
81
|
+
class ::PrefixTestAPIKey < Familia::Horreum
|
|
82
|
+
feature :relationships
|
|
83
|
+
prefix :ptapikey # Differs from config_name "prefix_test_a_p_i_key"
|
|
84
|
+
|
|
85
|
+
identifier_field :key_id
|
|
86
|
+
field :key_id
|
|
87
|
+
field :created_at
|
|
88
|
+
|
|
89
|
+
sorted_set :authorized_resources
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class ::PrefixTestAPIResource < Familia::Horreum
|
|
93
|
+
feature :relationships
|
|
94
|
+
|
|
95
|
+
identifier_field :resource_id
|
|
96
|
+
field :resource_id
|
|
97
|
+
field :name
|
|
98
|
+
field :created_at
|
|
99
|
+
|
|
100
|
+
participates_in PrefixTestAPIKey, :authorized_resources, score: :created_at
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Scenario 5: Namespaced class with explicit prefix
|
|
104
|
+
# Tests that demodularization doesn't interfere
|
|
105
|
+
module ::PrefixTestNS
|
|
106
|
+
class CustomDomain < Familia::Horreum
|
|
107
|
+
feature :relationships
|
|
108
|
+
prefix :ptnscustomdomain # Explicit, doesn't match "custom_domain"
|
|
109
|
+
|
|
110
|
+
identifier_field :domain_id
|
|
111
|
+
field :domain_id
|
|
112
|
+
field :created_at
|
|
113
|
+
|
|
114
|
+
participates_in PrefixTestSimpleModel, :participants, score: :created_at
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Scenario 6: OAuth2Provider-like - complex snake_case edge case
|
|
119
|
+
# config_name would be "o_auth2provider"
|
|
120
|
+
class ::PrefixTestOAuthProvider < Familia::Horreum
|
|
121
|
+
feature :relationships
|
|
122
|
+
prefix :ptoauthprovider # Differs from config_name
|
|
123
|
+
|
|
124
|
+
identifier_field :provider_id
|
|
125
|
+
field :provider_id
|
|
126
|
+
field :name
|
|
127
|
+
|
|
128
|
+
sorted_set :connected_users
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class ::PrefixTestOAuthUser < Familia::Horreum
|
|
132
|
+
feature :relationships
|
|
133
|
+
|
|
134
|
+
identifier_field :user_id
|
|
135
|
+
field :user_id
|
|
136
|
+
field :connected_at
|
|
137
|
+
|
|
138
|
+
participates_in PrefixTestOAuthProvider, :connected_users, score: :connected_at
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Setup test instances
|
|
142
|
+
@simple_model = PrefixTestSimpleModel.new(model_id: 'model_1', name: 'Test Model')
|
|
143
|
+
@simple_participant = PrefixTestSimpleParticipant.new(participant_id: 'part_1', created_at: Familia.now.to_i)
|
|
144
|
+
|
|
145
|
+
@matching_team = PrefixMatchingTeam.new(team_id: 'team_1', name: 'Engineering')
|
|
146
|
+
@matching_member = PrefixMatchingMember.new(member_id: 'member_1', joined_at: Familia.now.to_i)
|
|
147
|
+
|
|
148
|
+
@mismatched_domain = PrefixMismatchedDomain.new(
|
|
149
|
+
domain_id: 'cd_1',
|
|
150
|
+
display_domain: 'example.com',
|
|
151
|
+
created_at: Familia.now.to_i
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@api_key = PrefixTestAPIKey.new(key_id: 'key_1', created_at: Familia.now.to_i)
|
|
155
|
+
@api_resource = PrefixTestAPIResource.new(
|
|
156
|
+
resource_id: 'res_1',
|
|
157
|
+
name: 'Protected Resource',
|
|
158
|
+
created_at: Familia.now.to_i
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
@ns_domain = PrefixTestNS::CustomDomain.new(domain_id: 'otcd_1', created_at: Familia.now.to_i)
|
|
162
|
+
|
|
163
|
+
@oauth_provider = PrefixTestOAuthProvider.new(provider_id: 'oauth_1', name: 'Google')
|
|
164
|
+
@oauth_user = PrefixTestOAuthUser.new(user_id: 'ouser_1', connected_at: Familia.now.to_i)
|
|
165
|
+
|
|
166
|
+
# Scenario 1: Default prefix (no explicit prefix set)
|
|
167
|
+
|
|
168
|
+
## PrefixTestSimpleModel prefix equals config_name as symbol (default behavior)
|
|
169
|
+
PrefixTestSimpleModel.prefix
|
|
170
|
+
#=> :prefix_test_simple_model
|
|
171
|
+
|
|
172
|
+
## PrefixTestSimpleModel config_name is snake_case string
|
|
173
|
+
PrefixTestSimpleModel.config_name
|
|
174
|
+
#=> "prefix_test_simple_model"
|
|
175
|
+
|
|
176
|
+
## Default prefix matches config_name when converted
|
|
177
|
+
PrefixTestSimpleModel.prefix.to_s == PrefixTestSimpleModel.config_name
|
|
178
|
+
#=> true
|
|
179
|
+
|
|
180
|
+
## PrefixTestSimpleParticipant also has default prefix
|
|
181
|
+
PrefixTestSimpleParticipant.prefix
|
|
182
|
+
#=> :prefix_test_simple_participant
|
|
183
|
+
|
|
184
|
+
# Scenario 2: Explicit prefix matching config_name
|
|
185
|
+
|
|
186
|
+
## PrefixMatchingTeam has explicit prefix that matches config_name
|
|
187
|
+
PrefixMatchingTeam.prefix
|
|
188
|
+
#=> :prefix_matching_team
|
|
189
|
+
|
|
190
|
+
## PrefixMatchingTeam config_name matches prefix
|
|
191
|
+
PrefixMatchingTeam.config_name
|
|
192
|
+
#=> "prefix_matching_team"
|
|
193
|
+
|
|
194
|
+
## Matching prefix and config_name work the same
|
|
195
|
+
PrefixMatchingTeam.prefix.to_s == PrefixMatchingTeam.config_name
|
|
196
|
+
#=> true
|
|
197
|
+
|
|
198
|
+
# Scenario 3: THE BUG CASE - Explicit prefix differs from config_name
|
|
199
|
+
|
|
200
|
+
## PrefixMismatchedDomain has explicit prefix without underscore
|
|
201
|
+
PrefixMismatchedDomain.prefix
|
|
202
|
+
#=> :mismatcheddomain
|
|
203
|
+
|
|
204
|
+
## PrefixMismatchedDomain config_name has underscores (snake_case)
|
|
205
|
+
PrefixMismatchedDomain.config_name
|
|
206
|
+
#=> "prefix_mismatched_domain"
|
|
207
|
+
|
|
208
|
+
## PrefixMismatchedDomain prefix differs from config_name
|
|
209
|
+
PrefixMismatchedDomain.prefix.to_s == PrefixMismatchedDomain.config_name
|
|
210
|
+
#=> false
|
|
211
|
+
|
|
212
|
+
# Scenario 4: PrefixTestAPIKey prefix edge case
|
|
213
|
+
|
|
214
|
+
## PrefixTestAPIKey has explicit prefix
|
|
215
|
+
PrefixTestAPIKey.prefix
|
|
216
|
+
#=> :ptapikey
|
|
217
|
+
|
|
218
|
+
## PrefixTestAPIKey config_name follows snake_case convention
|
|
219
|
+
PrefixTestAPIKey.config_name
|
|
220
|
+
#=> "prefix_test_api_key"
|
|
221
|
+
|
|
222
|
+
## PrefixTestAPIKey prefix differs from config_name
|
|
223
|
+
PrefixTestAPIKey.prefix.to_s == PrefixTestAPIKey.config_name
|
|
224
|
+
#=> false
|
|
225
|
+
|
|
226
|
+
# Scenario 5: Namespaced class verification
|
|
227
|
+
|
|
228
|
+
## Namespaced PrefixTestNS::CustomDomain has explicit prefix
|
|
229
|
+
PrefixTestNS::CustomDomain.prefix
|
|
230
|
+
#=> :ptnscustomdomain
|
|
231
|
+
|
|
232
|
+
## Namespaced config_name only uses demodularized name
|
|
233
|
+
PrefixTestNS::CustomDomain.config_name
|
|
234
|
+
#=> "custom_domain"
|
|
235
|
+
|
|
236
|
+
## Namespaced prefix is distinct from config_name
|
|
237
|
+
PrefixTestNS::CustomDomain.prefix.to_s == PrefixTestNS::CustomDomain.config_name
|
|
238
|
+
#=> false
|
|
239
|
+
|
|
240
|
+
# Scenario 6: PrefixTestOAuthProvider edge case
|
|
241
|
+
|
|
242
|
+
## PrefixTestOAuthProvider has explicit prefix
|
|
243
|
+
PrefixTestOAuthProvider.prefix
|
|
244
|
+
#=> :ptoauthprovider
|
|
245
|
+
|
|
246
|
+
## PrefixTestOAuthProvider snake_case config_name
|
|
247
|
+
PrefixTestOAuthProvider.config_name
|
|
248
|
+
#=> "prefix_test_o_auth_provider"
|
|
249
|
+
|
|
250
|
+
## PrefixTestOAuthProvider prefix differs from config_name
|
|
251
|
+
PrefixTestOAuthProvider.prefix.to_s == PrefixTestOAuthProvider.config_name
|
|
252
|
+
#=> false
|
|
253
|
+
|
|
254
|
+
# Functional tests: Reverse lookups with mismatched prefix/config_name
|
|
255
|
+
|
|
256
|
+
## Save all test instances for functional tests
|
|
257
|
+
[@simple_model, @simple_participant, @matching_team, @matching_member,
|
|
258
|
+
@mismatched_domain, @api_key, @api_resource, @ns_domain,
|
|
259
|
+
@oauth_provider, @oauth_user].each(&:save)
|
|
260
|
+
true
|
|
261
|
+
#=> true
|
|
262
|
+
|
|
263
|
+
## Add PrefixTestSimpleParticipant to PrefixTestSimpleModel (default prefix case)
|
|
264
|
+
@simple_participant.add_to_prefix_test_simple_model_participants(@simple_model)
|
|
265
|
+
@simple_participant.in_prefix_test_simple_model_participants?(@simple_model)
|
|
266
|
+
#=> true
|
|
267
|
+
|
|
268
|
+
## PrefixTestSimpleParticipant reverse lookup finds PrefixTestSimpleModel instances
|
|
269
|
+
@simple_participant.prefix_test_simple_model_instances.map(&:identifier)
|
|
270
|
+
#=> ["model_1"]
|
|
271
|
+
|
|
272
|
+
## Add PrefixMatchingMember to PrefixMatchingTeam (matching prefix case)
|
|
273
|
+
@matching_member.add_to_prefix_matching_team_members(@matching_team)
|
|
274
|
+
@matching_member.in_prefix_matching_team_members?(@matching_team)
|
|
275
|
+
#=> true
|
|
276
|
+
|
|
277
|
+
## PrefixMatchingMember reverse lookup finds PrefixMatchingTeam instances
|
|
278
|
+
@matching_member.prefix_matching_team_instances.map(&:identifier)
|
|
279
|
+
#=> ["team_1"]
|
|
280
|
+
|
|
281
|
+
## Add PrefixMismatchedDomain to PrefixTestSimpleModel (THE BUG CASE - mismatched prefix)
|
|
282
|
+
@mismatched_domain.add_to_prefix_test_simple_model_participants(@simple_model)
|
|
283
|
+
@mismatched_domain.in_prefix_test_simple_model_participants?(@simple_model)
|
|
284
|
+
#=> true
|
|
285
|
+
|
|
286
|
+
## PrefixMismatchedDomain reverse lookup must find PrefixTestSimpleModel via prefix not config_name
|
|
287
|
+
@mismatched_domain.prefix_test_simple_model_instances.map(&:identifier)
|
|
288
|
+
#=> ["model_1"]
|
|
289
|
+
|
|
290
|
+
## PrefixMismatchedDomain ids method also works
|
|
291
|
+
@mismatched_domain.prefix_test_simple_model_ids
|
|
292
|
+
#=> ["model_1"]
|
|
293
|
+
|
|
294
|
+
## PrefixMismatchedDomain boolean check works
|
|
295
|
+
@mismatched_domain.prefix_test_simple_model?
|
|
296
|
+
#=> true
|
|
297
|
+
|
|
298
|
+
## PrefixMismatchedDomain count works
|
|
299
|
+
@mismatched_domain.prefix_test_simple_model_count
|
|
300
|
+
#=> 1
|
|
301
|
+
|
|
302
|
+
## Add PrefixTestAPIResource to PrefixTestAPIKey (mismatched prefix case)
|
|
303
|
+
@api_resource.add_to_prefix_test_api_key_authorized_resources(@api_key)
|
|
304
|
+
@api_resource.in_prefix_test_api_key_authorized_resources?(@api_key)
|
|
305
|
+
#=> true
|
|
306
|
+
|
|
307
|
+
## PrefixTestAPIResource reverse lookup finds PrefixTestAPIKey via prefix not config_name
|
|
308
|
+
@api_resource.prefix_test_api_key_instances.map(&:identifier)
|
|
309
|
+
#=> ["key_1"]
|
|
310
|
+
|
|
311
|
+
## Add PrefixTestNS::CustomDomain to PrefixTestSimpleModel (namespaced with mismatched prefix)
|
|
312
|
+
@ns_domain.add_to_prefix_test_simple_model_participants(@simple_model)
|
|
313
|
+
@ns_domain.in_prefix_test_simple_model_participants?(@simple_model)
|
|
314
|
+
#=> true
|
|
315
|
+
|
|
316
|
+
## PrefixTestNS::CustomDomain reverse lookup finds PrefixTestSimpleModel
|
|
317
|
+
@ns_domain.prefix_test_simple_model_instances.map(&:identifier)
|
|
318
|
+
#=> ["model_1"]
|
|
319
|
+
|
|
320
|
+
## Add PrefixTestOAuthUser to PrefixTestOAuthProvider (mismatched prefix case)
|
|
321
|
+
@oauth_user.add_to_prefix_test_o_auth_provider_connected_users(@oauth_provider)
|
|
322
|
+
@oauth_user.in_prefix_test_o_auth_provider_connected_users?(@oauth_provider)
|
|
323
|
+
#=> true
|
|
324
|
+
|
|
325
|
+
## PrefixTestOAuthUser reverse lookup finds PrefixTestOAuthProvider via prefix
|
|
326
|
+
@oauth_user.prefix_test_o_auth_provider_instances.map(&:identifier)
|
|
327
|
+
#=> ["oauth_1"]
|
|
328
|
+
|
|
329
|
+
# Verify dbkey format uses prefix, not config_name
|
|
330
|
+
|
|
331
|
+
## PrefixTestSimpleModel dbkey uses default prefix (which equals config_name)
|
|
332
|
+
@simple_model.dbkey.start_with?("prefix_test_simple_model:")
|
|
333
|
+
#=> true
|
|
334
|
+
|
|
335
|
+
## PrefixMismatchedDomain dbkey uses explicit prefix
|
|
336
|
+
@mismatched_domain.dbkey.start_with?("mismatcheddomain:")
|
|
337
|
+
#=> true
|
|
338
|
+
|
|
339
|
+
## PrefixTestAPIKey dbkey uses explicit prefix
|
|
340
|
+
@api_key.dbkey.start_with?("ptapikey:")
|
|
341
|
+
#=> true
|
|
342
|
+
|
|
343
|
+
## PrefixTestNS::CustomDomain dbkey uses explicit prefix
|
|
344
|
+
@ns_domain.dbkey.start_with?("ptnscustomdomain:")
|
|
345
|
+
#=> true
|
|
346
|
+
|
|
347
|
+
## PrefixTestOAuthProvider dbkey uses explicit prefix
|
|
348
|
+
@oauth_provider.dbkey.start_with?("ptoauthprovider:")
|
|
349
|
+
#=> true
|
|
350
|
+
|
|
351
|
+
# Verify participation tracking uses prefix in keys
|
|
352
|
+
|
|
353
|
+
## PrefixTestSimpleParticipant participations include key with prefix_test_simple_model prefix
|
|
354
|
+
@simple_participant_keys = @simple_participant.participations.members
|
|
355
|
+
@simple_participant_keys.any? { |k| k.start_with?("prefix_test_simple_model:") }
|
|
356
|
+
#=> true
|
|
357
|
+
|
|
358
|
+
## PrefixMismatchedDomain participations include key with prefix_test_simple_model prefix
|
|
359
|
+
@mismatched_domain_keys = @mismatched_domain.participations.members
|
|
360
|
+
@mismatched_domain_keys.any? { |k| k.start_with?("prefix_test_simple_model:") }
|
|
361
|
+
#=> true
|
|
362
|
+
|
|
363
|
+
## PrefixTestAPIResource participations include key with ptapikey prefix (not prefix_test_api_key)
|
|
364
|
+
@api_resource_keys = @api_resource.participations.members
|
|
365
|
+
@api_resource_keys.any? { |k| k.start_with?("ptapikey:") }
|
|
366
|
+
#=> true
|
|
367
|
+
|
|
368
|
+
## PrefixTestOAuthUser participations include key with ptoauthprovider prefix
|
|
369
|
+
@oauth_user_keys = @oauth_user.participations.members
|
|
370
|
+
@oauth_user_keys.any? { |k| k.start_with?("ptoauthprovider:") }
|
|
371
|
+
#=> true
|
|
372
|
+
|
|
373
|
+
# Bug fix verification: Keys use prefix, NOT config_name
|
|
374
|
+
# If the old bug existed, these tests would fail because config_name would be
|
|
375
|
+
# used for matching but keys are stored with prefix
|
|
376
|
+
|
|
377
|
+
## PrefixTestAPIResource keys do NOT contain config_name pattern (prefix_test_api_key)
|
|
378
|
+
# This verifies that the key is "ptapikey:..." not "prefix_test_api_key:..."
|
|
379
|
+
@api_resource_keys.none? { |k| k.start_with?("prefix_test_api_key:") }
|
|
380
|
+
#=> true
|
|
381
|
+
|
|
382
|
+
## PrefixTestOAuthUser keys do NOT contain config_name pattern
|
|
383
|
+
# This verifies that the key is "ptoauthprovider:..." not "prefix_test_o_auth_provider:..."
|
|
384
|
+
@oauth_user_keys.none? { |k| k.start_with?("prefix_test_o_auth_provider:") }
|
|
385
|
+
#=> true
|
|
386
|
+
|
|
387
|
+
## PrefixTestAPIKey prefix is different from config_name
|
|
388
|
+
# This is the critical condition for the bug: prefix != config_name
|
|
389
|
+
PrefixTestAPIKey.prefix.to_s != PrefixTestAPIKey.config_name
|
|
390
|
+
#=> true
|
|
391
|
+
|
|
392
|
+
## PrefixTestOAuthProvider prefix is different from config_name
|
|
393
|
+
PrefixTestOAuthProvider.prefix.to_s != PrefixTestOAuthProvider.config_name
|
|
394
|
+
#=> true
|
|
395
|
+
|
|
396
|
+
## participating_ids_for_target finds IDs when prefix differs from config_name
|
|
397
|
+
# This is the core bug fix test - the method must use prefix, not config_name
|
|
398
|
+
@api_resource.participating_ids_for_target(PrefixTestAPIKey).include?("key_1")
|
|
399
|
+
#=> true
|
|
400
|
+
|
|
401
|
+
## participating_in_target? returns true when prefix differs from config_name
|
|
402
|
+
@api_resource.participating_in_target?(PrefixTestAPIKey)
|
|
403
|
+
#=> true
|
|
404
|
+
|
|
405
|
+
## PrefixTestOAuthUser also finds its target despite prefix != config_name
|
|
406
|
+
@oauth_user.participating_ids_for_target(PrefixTestOAuthProvider).include?("oauth_1")
|
|
407
|
+
#=> true
|
|
408
|
+
|
|
409
|
+
## PrefixTestOAuthUser participating_in_target? also works
|
|
410
|
+
@oauth_user.participating_in_target?(PrefixTestOAuthProvider)
|
|
411
|
+
#=> true
|
|
412
|
+
|
|
413
|
+
## Cleanup test data completes without errors
|
|
414
|
+
[@simple_model, @simple_participant, @matching_team, @matching_member,
|
|
415
|
+
@mismatched_domain, @api_key, @api_resource, @ns_domain,
|
|
416
|
+
@oauth_provider, @oauth_user].compact.each { |obj| obj.destroy if obj.respond_to?(:destroy) && obj.exists? }
|
|
417
|
+
true
|
|
418
|
+
#=> true
|