Skip to content

Commit cb7971a

Browse files
BugFix: '<attribute>' and '<attribute>_id' conflict. (#1709)
* Fixes '<attribute>' and '<attribute>_id' conflict. - AttributeAssigner refactored to ignore a matching attribute alias, if it also matches the name of another attribute. - Tests added for attribute assignment. * Refactored 'AttributeAssigner' to use :attribute_names method - Gemfile update from rebasing on main. * Consolidate attribute aliases acceptance specs --------- Co-authored-by: Neil Carvalho <[email protected]>
1 parent 89d60e0 commit cb7971a

File tree

4 files changed

+319
-46
lines changed

4 files changed

+319
-46
lines changed

lib/factory_bot/attribute_assigner.rb

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def attribute_names_to_assign
7272
non_ignored_attribute_names +
7373
override_names -
7474
ignored_attribute_names -
75-
alias_names_to_ignore
75+
aliased_attribute_names_to_ignore
7676
end
7777

7878
def non_ignored_attribute_names
@@ -91,22 +91,39 @@ def override_names
9191
@evaluator.__override_names__
9292
end
9393

94+
def attribute_names
95+
@attribute_list.names
96+
end
97+
9498
def hash_instance_methods_to_respond_to
95-
@attribute_list.names + override_names + @build_class.instance_methods
99+
attribute_names + override_names + @build_class.instance_methods
96100
end
97101

98-
def alias_names_to_ignore
102+
##
103+
# Creat a list of attribute names that will be
104+
# overridden by an alias, so any defaults can
105+
# ignored.
106+
#
107+
def aliased_attribute_names_to_ignore
99108
@attribute_list.non_ignored.flat_map { |attribute|
100109
override_names.map do |override|
101-
attribute.name if ignorable_alias?(attribute, override)
110+
attribute.name if aliased_attribute?(attribute, override)
102111
end
103112
}.compact
104113
end
105114

106-
def ignorable_alias?(attribute, override)
107-
attribute.alias_for?(override) &&
108-
attribute.name != override &&
109-
!ignored_attribute_names.include?(override)
115+
##
116+
# Is the override an alias for the attribute and not the
117+
# actual name of another attribute?
118+
#
119+
# Note: Checking against the names of all attributes, resolves any
120+
# issues with having both <attribute> and <attribute>_id
121+
# in the same factory.
122+
#
123+
def aliased_attribute?(attribute, override)
124+
return false if attribute_names.include?(override)
125+
126+
attribute.alias_for?(override)
110127
end
111128
end
112129
end

spec/acceptance/aliases_spec.rb

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 255 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,276 @@
11
describe "attribute aliases" do
2-
before do
3-
define_model("User", name: :string, age: :integer)
2+
around do |example|
3+
original_aliases = FactoryBot.aliases.dup
4+
example.run
5+
ensure
6+
FactoryBot.aliases.clear
7+
FactoryBot.aliases.concat(original_aliases)
8+
end
9+
10+
describe "basic alias functionality" do
11+
it "allows using different parameter names that map to model attributes" do
12+
FactoryBot.aliases << [/author_name/, "name"]
13+
define_model("User", name: :string, author_name: :string)
14+
15+
FactoryBot.define do
16+
factory :user do
17+
name { "Default Name" }
18+
end
19+
end
20+
21+
user = FactoryBot.create(:user, author_name: "Custom Name")
422

5-
define_model("Post", user_id: :integer) do
6-
belongs_to :user
23+
expect(user.author_name).to eq "Custom Name"
24+
expect(user.name).to be_nil
725
end
826

9-
FactoryBot.define do
10-
factory :user do
11-
factory :user_with_name do
12-
name { "John Doe" }
27+
it "ignores factory defaults when alias is used" do
28+
FactoryBot.aliases << [/display_name/, "name"]
29+
define_model("User", name: :string, display_name: :string)
30+
31+
FactoryBot.define do
32+
factory :user do
33+
name { "Factory Name" }
1334
end
1435
end
1536

16-
factory :post do
17-
user
37+
user = FactoryBot.create(:user, display_name: "Override Name")
38+
39+
expect(user.display_name).to eq "Override Name"
40+
expect(user.name).to be_nil
41+
end
42+
end
43+
44+
describe "built-in _id aliases" do
45+
it "automatically alias between associations and foreign keys" do
46+
define_model("User", name: :string)
47+
define_model("Post", user_id: :integer) do
48+
belongs_to :user, optional: true
1849
end
1950

20-
factory :post_with_named_user, class: Post do
21-
user factory: :user_with_name, age: 20
51+
FactoryBot.define do
52+
factory :user do
53+
name { "Test User" }
54+
end
55+
56+
factory :post
2257
end
58+
59+
user = FactoryBot.create(:user)
60+
61+
post_with_direct_id = FactoryBot.create(:post, user_id: user.id)
62+
expect(post_with_direct_id.user_id).to eq user.id
63+
64+
post_with_association = FactoryBot.create(:post, user: user)
65+
expect(post_with_association.user_id).to eq user.id
66+
end
67+
68+
it "prevents conflicts between associations and foreign keys" do
69+
define_model("User", name: :string, age: :integer)
70+
define_model("Post", user_id: :integer) do
71+
belongs_to :user
72+
end
73+
74+
FactoryBot.define do
75+
factory :user do
76+
factory :user_with_name do
77+
name { "John Doe" }
78+
end
79+
end
80+
81+
factory :post do
82+
user
83+
end
84+
end
85+
86+
post_with_foreign_key = FactoryBot.build(:post, user_id: 1)
87+
88+
expect(post_with_foreign_key.user_id).to eq 1
89+
end
90+
91+
it "allows passing attributes to associated factories" do
92+
define_model("User", name: :string, age: :integer)
93+
define_model("Post", user_id: :integer) do
94+
belongs_to :user
95+
end
96+
97+
FactoryBot.define do
98+
factory :user do
99+
factory :user_with_name do
100+
name { "John Doe" }
101+
end
102+
end
103+
104+
factory :post do
105+
user
106+
end
107+
108+
factory :post_with_named_user, class: Post do
109+
user factory: :user_with_name, age: 20
110+
end
111+
end
112+
113+
created_user = FactoryBot.create(:post_with_named_user).user
114+
115+
expect(created_user.name).to eq "John Doe"
116+
expect(created_user.age).to eq 20
23117
end
24118
end
25119

26-
context "assigning an association by foreign key" do
27-
subject { FactoryBot.build(:post, user_id: 1) }
120+
describe "custom alias patterns" do
121+
it "supports regex patterns with capture groups" do
122+
FactoryBot.aliases << [/(.+)_alias/, '\1']
123+
define_model("User", name: :string, name_alias: :string, email: :string, email_alias: :string)
124+
125+
FactoryBot.define do
126+
factory :user do
127+
name { "Default Name" }
128+
email { "[email protected]" }
129+
end
130+
end
131+
132+
user = FactoryBot.create(:user, name_alias: "Aliased Name", email_alias: "[email protected]")
28133

29-
it "doesn't assign both an association and its foreign key" do
30-
expect(subject.user_id).to eq 1
134+
expect(user.name).to be_nil
135+
expect(user.email).to be_nil
136+
expect(user.name_alias).to eq "Aliased Name"
137+
expect(user.email_alias).to eq "[email protected]"
138+
end
139+
140+
it "supports multiple alias patterns working together" do
141+
FactoryBot.aliases << [/primary_(.+)/, '\1']
142+
FactoryBot.aliases << [/alt_name/, "name"]
143+
define_model("User", name: :string, email: :string, primary_email: :string, alt_name: :string)
144+
145+
FactoryBot.define do
146+
factory :user do
147+
name { "Default Name" }
148+
email { "[email protected]" }
149+
end
150+
end
151+
152+
user = FactoryBot.create(:user, primary_email: "[email protected]", alt_name: "Alternative Name")
153+
154+
expect(user.name).to be_nil
155+
expect(user.email).to be_nil
156+
expect(user.primary_email).to eq "[email protected]"
157+
expect(user.alt_name).to eq "Alternative Name"
158+
end
159+
160+
it "works with custom foreign key names" do
161+
FactoryBot.aliases << [/owner_id/, "user_id"]
162+
define_model("User", name: :string)
163+
define_model("Document", user_id: :integer, owner_id: :integer, title: :string) do
164+
belongs_to :user, optional: true
165+
end
166+
167+
FactoryBot.define do
168+
factory :user do
169+
name { "Test User" }
170+
end
171+
172+
factory :document do
173+
title { "Test Document" }
174+
end
175+
end
176+
177+
document_owner = FactoryBot.create(:user)
178+
document = FactoryBot.create(:document, owner_id: document_owner.id)
179+
180+
expect(document.user_id).to be_nil
181+
expect(document.owner_id).to eq document_owner.id
182+
expect(document.title).to eq "Test Document"
183+
end
184+
end
185+
186+
describe "edge cases" do
187+
it "allows setting nil values through aliases" do
188+
FactoryBot.aliases << [/clear_name/, "name"]
189+
define_model("User", name: :string, clear_name: :string)
190+
191+
FactoryBot.define do
192+
factory :user do
193+
name { "Default Name" }
194+
end
195+
end
196+
197+
user = FactoryBot.create(:user, clear_name: nil)
198+
199+
expect(user.name).to be_nil
200+
expect(user.clear_name).to be_nil
201+
end
202+
203+
context "when both alias and target attributes exist on model" do
204+
it "ignores factory defaults for target when alias is used" do
205+
FactoryBot.aliases << [/one/, "two"]
206+
define_model("User", two: :string, one: :string)
207+
208+
FactoryBot.define do
209+
factory :user do
210+
two { "set value" }
211+
end
212+
end
213+
214+
user = FactoryBot.create(:user, one: "override")
215+
216+
expect(user.one).to eq "override"
217+
expect(user.two).to be_nil
218+
end
219+
end
220+
end
221+
222+
describe "with attributes_for strategy" do
223+
it "includes alias names in hash and ignores aliased factory defaults" do
224+
FactoryBot.aliases << [/author_name/, "name"]
225+
define_model("User", name: :string, author_name: :string)
226+
227+
FactoryBot.define do
228+
factory :user do
229+
name { "Default Name" }
230+
end
231+
end
232+
233+
attributes = FactoryBot.attributes_for(:user, author_name: "Custom Name")
234+
235+
expect(attributes[:author_name]).to eq "Custom Name"
236+
expect(attributes[:name]).to be_nil
31237
end
32238
end
33239

34-
context "assigning an association by passing factory" do
35-
subject { FactoryBot.create(:post_with_named_user).user }
240+
describe "attribute conflicts with _id patterns" do
241+
it "doesn't set factory defaults when alias is used instead of target attribute" do
242+
define_model("User", name: :string, response_id: :integer)
243+
244+
FactoryBot.define do
245+
factory :user do
246+
name { "orig name" }
247+
response_id { 42 }
248+
end
249+
end
250+
251+
attributes = FactoryBot.attributes_for(:user, name: "new name", response: 13.75)
252+
253+
expect(attributes[:name]).to eq "new name"
254+
expect(attributes[:response]).to eq 13.75
255+
expect(attributes[:response_id]).to be_nil
256+
end
257+
258+
it "allows setting both attribute and attribute_id without conflicts" do
259+
define_model("User", name: :string, response: :string, response_id: :float)
260+
261+
FactoryBot.define do
262+
factory :user do
263+
name { "orig name" }
264+
response { "orig response" }
265+
response_id { 42 }
266+
end
267+
end
268+
269+
user = FactoryBot.build(:user, name: "new name", response: "new response", response_id: 13.75)
36270

37-
it "assigns attributes correctly" do
38-
expect(subject.name).to eq "John Doe"
39-
expect(subject.age).to eq 20
271+
expect(user.name).to eq "new name"
272+
expect(user.response).to eq "new response"
273+
expect(user.response_id).to eq 13.75
40274
end
41275
end
42276
end

0 commit comments

Comments
 (0)