seed_dump 3.3.1 → 3.4.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.
@@ -2,71 +2,110 @@ require 'spec_helper'
2
2
 
3
3
  describe SeedDump do
4
4
  describe '.dump_using_environment' do
5
- before(:all) do
6
- create_db
7
- end
5
+ # Schema creation and model loading are handled in spec_helper's before(:suite).
8
6
 
9
7
  before(:each) do
10
- Rails.application.eager_load!
11
-
8
+ # Clean DB and create a fresh sample before each example
9
+ DatabaseCleaner.start
12
10
  FactoryBot.create(:sample)
13
11
  end
14
12
 
13
+ after(:each) do
14
+ # Clean DB after each example
15
+ DatabaseCleaner.clean
16
+ end
17
+
18
+
15
19
  describe 'APPEND' do
16
20
  it "should specify append as true if the APPEND env var is 'true'" do
17
- SeedDump.should_receive(:dump).with(anything, include(append: true))
18
-
21
+ expect(SeedDump).to receive(:dump).with(anything, include(append: true))
22
+ # Need to stub dump for other models if they exist in this context
23
+ allow(SeedDump).to receive(:dump).with(AnotherSample, anything) if defined?(AnotherSample)
24
+ allow(SeedDump).to receive(:dump).with(YetAnotherSample, anything) if defined?(YetAnotherSample)
19
25
  SeedDump.dump_using_environment('APPEND' => 'true')
20
26
  end
21
27
 
22
28
  it "should specify append as true if the APPEND env var is 'TRUE'" do
23
- SeedDump.should_receive(:dump).with(anything, include(append: true))
24
-
29
+ expect(SeedDump).to receive(:dump).with(anything, include(append: true))
30
+ allow(SeedDump).to receive(:dump).with(AnotherSample, anything) if defined?(AnotherSample)
31
+ allow(SeedDump).to receive(:dump).with(YetAnotherSample, anything) if defined?(YetAnotherSample)
25
32
  SeedDump.dump_using_environment('APPEND' => 'TRUE')
26
33
  end
27
34
 
28
35
  it "should specify append as false the first time if the APPEND env var is not 'true' (and true after that)" do
29
36
  FactoryBot.create(:another_sample)
30
-
31
- SeedDump.should_receive(:dump).with(anything, include(append: false)).ordered
32
- SeedDump.should_receive(:dump).with(anything, include(append: true)).ordered
33
-
34
- SeedDump.dump_using_environment('APPEND' => 'false')
37
+ expect(SeedDump).to receive(:dump).with(Sample, include(append: false)).ordered
38
+ expect(SeedDump).to receive(:dump).with(AnotherSample, include(append: true)).ordered
39
+ # Explicitly set MODELS to control order and prevent other models interfering
40
+ SeedDump.dump_using_environment('APPEND' => 'false', 'MODELS' => 'Sample,AnotherSample')
35
41
  end
36
42
  end
37
43
 
38
44
  describe 'BATCH_SIZE' do
39
45
  it 'should pass along the specified batch size' do
40
- SeedDump.should_receive(:dump).with(anything, include(batch_size: 17))
41
-
46
+ expect(SeedDump).to receive(:dump).with(anything, include(batch_size: 17))
47
+ allow(SeedDump).to receive(:dump).with(AnotherSample, anything) if defined?(AnotherSample)
48
+ allow(SeedDump).to receive(:dump).with(YetAnotherSample, anything) if defined?(YetAnotherSample)
42
49
  SeedDump.dump_using_environment('BATCH_SIZE' => '17')
43
50
  end
44
51
 
45
52
  it 'should pass along a nil batch size if BATCH_SIZE is not specified' do
46
- SeedDump.should_receive(:dump).with(anything, include(batch_size: nil))
47
-
53
+ expect(SeedDump).to receive(:dump).with(anything, include(batch_size: nil))
54
+ allow(SeedDump).to receive(:dump).with(AnotherSample, anything) if defined?(AnotherSample)
55
+ allow(SeedDump).to receive(:dump).with(YetAnotherSample, anything) if defined?(YetAnotherSample)
48
56
  SeedDump.dump_using_environment
49
57
  end
50
58
  end
51
59
 
52
60
  describe 'EXCLUDE' do
53
61
  it 'should pass along any attributes to be excluded' do
54
- SeedDump.should_receive(:dump).with(anything, include(exclude: [:baggins, :saggins]))
55
-
62
+ expect(SeedDump).to receive(:dump).with(anything, include(exclude: [:baggins, :saggins]))
63
+ allow(SeedDump).to receive(:dump).with(AnotherSample, anything) if defined?(AnotherSample)
64
+ allow(SeedDump).to receive(:dump).with(YetAnotherSample, anything) if defined?(YetAnotherSample)
56
65
  SeedDump.dump_using_environment('EXCLUDE' => 'baggins,saggins')
57
66
  end
67
+
68
+ it 'should pass an empty array when EXCLUDE is set to empty string (issue #147)' do
69
+ expect(SeedDump).to receive(:dump).with(anything, include(exclude: []))
70
+ allow(SeedDump).to receive(:dump).with(AnotherSample, anything) if defined?(AnotherSample)
71
+ allow(SeedDump).to receive(:dump).with(YetAnotherSample, anything) if defined?(YetAnotherSample)
72
+ SeedDump.dump_using_environment('EXCLUDE' => '')
73
+ end
74
+
75
+ it 'should pass nil when EXCLUDE is not set (to use default excludes)' do
76
+ expect(SeedDump).to receive(:dump).with(anything, include(exclude: nil))
77
+ allow(SeedDump).to receive(:dump).with(AnotherSample, anything) if defined?(AnotherSample)
78
+ allow(SeedDump).to receive(:dump).with(YetAnotherSample, anything) if defined?(YetAnotherSample)
79
+ SeedDump.dump_using_environment
80
+ end
81
+
82
+ it 'should pass an empty array when INCLUDE_ALL is true (issue #147)' do
83
+ expect(SeedDump).to receive(:dump).with(anything, include(exclude: []))
84
+ allow(SeedDump).to receive(:dump).with(AnotherSample, anything) if defined?(AnotherSample)
85
+ allow(SeedDump).to receive(:dump).with(YetAnotherSample, anything) if defined?(YetAnotherSample)
86
+ SeedDump.dump_using_environment('INCLUDE_ALL' => 'true')
87
+ end
88
+
89
+ it 'should let explicit EXCLUDE override INCLUDE_ALL' do
90
+ expect(SeedDump).to receive(:dump).with(anything, include(exclude: [:some_field]))
91
+ allow(SeedDump).to receive(:dump).with(AnotherSample, anything) if defined?(AnotherSample)
92
+ allow(SeedDump).to receive(:dump).with(YetAnotherSample, anything) if defined?(YetAnotherSample)
93
+ SeedDump.dump_using_environment('INCLUDE_ALL' => 'true', 'EXCLUDE' => 'some_field')
94
+ end
58
95
  end
59
96
 
60
97
  describe 'FILE' do
61
98
  it 'should pass the FILE parameter to the dump method correctly' do
62
- SeedDump.should_receive(:dump).with(anything, include(file: 'blargle'))
63
-
99
+ expect(SeedDump).to receive(:dump).with(anything, include(file: 'blargle'))
100
+ allow(SeedDump).to receive(:dump).with(AnotherSample, anything) if defined?(AnotherSample)
101
+ allow(SeedDump).to receive(:dump).with(YetAnotherSample, anything) if defined?(YetAnotherSample)
64
102
  SeedDump.dump_using_environment('FILE' => 'blargle')
65
103
  end
66
104
 
67
105
  it 'should pass db/seeds.rb as the file parameter if no FILE is specified' do
68
- SeedDump.should_receive(:dump).with(anything, include(file: 'db/seeds.rb'))
69
-
106
+ expect(SeedDump).to receive(:dump).with(anything, include(file: 'db/seeds.rb'))
107
+ allow(SeedDump).to receive(:dump).with(AnotherSample, anything) if defined?(AnotherSample)
108
+ allow(SeedDump).to receive(:dump).with(YetAnotherSample, anything) if defined?(YetAnotherSample)
70
109
  SeedDump.dump_using_environment
71
110
  end
72
111
  end
@@ -74,15 +113,100 @@ describe SeedDump do
74
113
  describe 'LIMIT' do
75
114
  it 'should apply the specified limit to the records' do
76
115
  relation_double = double('ActiveRecord relation double')
77
- Sample.should_receive(:limit).with(5).and_return(relation_double)
116
+ allow(Sample).to receive(:limit).with(5).and_return(relation_double)
117
+ expect(SeedDump).to receive(:dump).with(relation_double, anything)
118
+ # Allow other calls if necessary
119
+ allow(SeedDump).to receive(:dump).with(instance_of(Class), anything) unless relation_double.is_a?(Class)
78
120
 
79
- SeedDump.should_receive(:dump).with(relation_double, anything)
80
- SeedDump.stub(:dump)
81
121
 
82
122
  SeedDump.dump_using_environment('LIMIT' => '5')
83
123
  end
84
124
  end
85
125
 
126
+ describe 'MODEL_LIMITS (issue #142)' do
127
+ # MODEL_LIMITS allows per-model limit overrides to prevent LIMIT from breaking
128
+ # associations. For example, if Teacher has_many Students, you can set
129
+ # MODEL_LIMITS=Teacher:0 to dump all teachers while limiting other models.
130
+
131
+ it 'should apply per-model limit when specified' do
132
+ FactoryBot.create(:another_sample)
133
+
134
+ sample_relation = double('Sample relation')
135
+ another_sample_relation = double('AnotherSample relation')
136
+
137
+ allow(Sample).to receive(:limit).with(5).and_return(sample_relation)
138
+ allow(AnotherSample).to receive(:limit).with(20).and_return(another_sample_relation)
139
+
140
+ expect(SeedDump).to receive(:dump).with(sample_relation, anything)
141
+ expect(SeedDump).to receive(:dump).with(another_sample_relation, anything)
142
+
143
+ SeedDump.dump_using_environment(
144
+ 'MODELS' => 'Sample,AnotherSample',
145
+ 'MODEL_LIMITS' => 'Sample:5,AnotherSample:20'
146
+ )
147
+ end
148
+
149
+ it 'should interpret 0 as unlimited (dump all records)' do
150
+ # When MODEL_LIMITS=Sample:0, Sample should not have limit applied
151
+ expect(Sample).not_to receive(:limit)
152
+ expect(SeedDump).to receive(:dump).with(Sample, anything)
153
+
154
+ SeedDump.dump_using_environment(
155
+ 'MODELS' => 'Sample',
156
+ 'MODEL_LIMITS' => 'Sample:0'
157
+ )
158
+ end
159
+
160
+ it 'should fall back to global LIMIT for models not in MODEL_LIMITS' do
161
+ FactoryBot.create(:another_sample)
162
+
163
+ # Sample has specific limit of 5, AnotherSample falls back to global LIMIT of 10
164
+ sample_relation = double('Sample relation')
165
+ another_sample_relation = double('AnotherSample relation')
166
+
167
+ allow(Sample).to receive(:limit).with(5).and_return(sample_relation)
168
+ allow(AnotherSample).to receive(:limit).with(10).and_return(another_sample_relation)
169
+
170
+ expect(SeedDump).to receive(:dump).with(sample_relation, anything)
171
+ expect(SeedDump).to receive(:dump).with(another_sample_relation, anything)
172
+
173
+ SeedDump.dump_using_environment(
174
+ 'MODELS' => 'Sample,AnotherSample',
175
+ 'LIMIT' => '10',
176
+ 'MODEL_LIMITS' => 'Sample:5'
177
+ )
178
+ end
179
+
180
+ it 'should work with MODEL_LIMITS alone (no global LIMIT)' do
181
+ FactoryBot.create(:another_sample)
182
+
183
+ # Sample has limit of 5, AnotherSample has no limit (dumps all)
184
+ sample_relation = double('Sample relation')
185
+
186
+ allow(Sample).to receive(:limit).with(5).and_return(sample_relation)
187
+ expect(AnotherSample).not_to receive(:limit)
188
+
189
+ expect(SeedDump).to receive(:dump).with(sample_relation, anything)
190
+ expect(SeedDump).to receive(:dump).with(AnotherSample, anything)
191
+
192
+ SeedDump.dump_using_environment(
193
+ 'MODELS' => 'Sample,AnotherSample',
194
+ 'MODEL_LIMITS' => 'Sample:5'
195
+ )
196
+ end
197
+
198
+ it 'should handle whitespace in MODEL_LIMITS' do
199
+ sample_relation = double('Sample relation')
200
+ allow(Sample).to receive(:limit).with(5).and_return(sample_relation)
201
+ expect(SeedDump).to receive(:dump).with(sample_relation, anything)
202
+
203
+ SeedDump.dump_using_environment(
204
+ 'MODELS' => 'Sample',
205
+ 'MODEL_LIMITS' => ' Sample : 5 '
206
+ )
207
+ end
208
+ end
209
+
86
210
  ['', 'S'].each do |model_suffix|
87
211
  model_env = 'MODEL' + model_suffix
88
212
 
@@ -90,11 +214,8 @@ describe SeedDump do
90
214
  context "if #{model_env} is not specified" do
91
215
  it "should dump all non-empty models" do
92
216
  FactoryBot.create(:another_sample)
93
-
94
- [Sample, AnotherSample].each do |model|
95
- SeedDump.should_receive(:dump).with(model, anything)
96
- end
97
-
217
+ expect(SeedDump).to receive(:dump).with(Sample, anything)
218
+ expect(SeedDump).to receive(:dump).with(AnotherSample, anything)
98
219
  SeedDump.dump_using_environment
99
220
  end
100
221
  end
@@ -102,15 +223,16 @@ describe SeedDump do
102
223
  context "if #{model_env} is specified" do
103
224
  it "should dump only the specified model" do
104
225
  FactoryBot.create(:another_sample)
105
-
106
- SeedDump.should_receive(:dump).with(Sample, anything)
107
-
226
+ expect(SeedDump).to receive(:dump).with(Sample, anything)
227
+ # Ensure the other model is NOT dumped
228
+ expect(SeedDump).not_to receive(:dump).with(AnotherSample, anything)
108
229
  SeedDump.dump_using_environment(model_env => 'Sample')
109
230
  end
110
231
 
111
232
  it "should not dump empty models" do
112
- SeedDump.should_not_receive(:dump).with(EmptyModel, anything)
113
-
233
+ expect(SeedDump).not_to receive(:dump).with(EmptyModel, anything)
234
+ # Ensure Sample is still dumped
235
+ expect(SeedDump).to receive(:dump).with(Sample, anything)
114
236
  SeedDump.dump_using_environment(model_env => 'EmptyModel, Sample')
115
237
  end
116
238
  end
@@ -120,25 +242,372 @@ describe SeedDump do
120
242
  describe "MODELS_EXCLUDE" do
121
243
  it "should dump all non-empty models except the specified models" do
122
244
  FactoryBot.create(:another_sample)
245
+ expect(SeedDump).to receive(:dump).with(Sample, anything)
246
+ # Ensure the excluded model is NOT dumped
247
+ expect(SeedDump).not_to receive(:dump).with(AnotherSample, anything)
248
+ SeedDump.dump_using_environment('MODELS_EXCLUDE' => 'AnotherSample')
249
+ end
250
+ end
123
251
 
124
- SeedDump.should_receive(:dump).with(Sample, anything)
252
+ describe 'model names ending in s (issue #121)' do
253
+ # Model names like "Boss" are incorrectly singularized to "Bos" when
254
+ # processing MODELS=Boss, causing NameError: uninitialized constant Bos.
255
+ # The fix should use the exact model name if it resolves to a valid constant.
125
256
 
126
- SeedDump.dump_using_environment('MODELS_EXCLUDE' => 'AnotherSample')
257
+ it 'should handle model name "Boss" without extra s' do
258
+ FactoryBot.create(:boss)
259
+ expect(SeedDump).to receive(:dump).with(Boss, anything)
260
+ SeedDump.dump_using_environment('MODELS' => 'Boss')
261
+ end
262
+
263
+ it 'should handle model name "boss" (lowercase) without extra s' do
264
+ FactoryBot.create(:boss)
265
+ expect(SeedDump).to receive(:dump).with(Boss, anything)
266
+ SeedDump.dump_using_environment('MODELS' => 'boss')
267
+ end
268
+
269
+ it 'should handle MODELS_EXCLUDE with model names ending in s' do
270
+ FactoryBot.create(:boss)
271
+ expect(SeedDump).to receive(:dump).with(Sample, anything)
272
+ expect(SeedDump).not_to receive(:dump).with(Boss, anything)
273
+ SeedDump.dump_using_environment('MODELS_EXCLUDE' => 'Boss')
274
+ end
275
+
276
+ it 'should still handle plural model names (e.g., "samples" -> Sample)' do
277
+ expect(SeedDump).to receive(:dump).with(Sample, anything)
278
+ SeedDump.dump_using_environment('MODELS' => 'samples')
279
+ end
280
+
281
+ it 'should still handle plural model names in MODELS_EXCLUDE' do
282
+ FactoryBot.create(:another_sample)
283
+ expect(SeedDump).to receive(:dump).with(AnotherSample, anything)
284
+ expect(SeedDump).not_to receive(:dump).with(Sample, anything)
285
+ SeedDump.dump_using_environment('MODELS_EXCLUDE' => 'samples')
286
+ end
287
+ end
288
+
289
+ describe 'INSERT_ALL (issue #153)' do
290
+ it "should specify insert_all as true if the INSERT_ALL env var is 'true'" do
291
+ expect(SeedDump).to receive(:dump).with(anything, include(insert_all: true))
292
+ SeedDump.dump_using_environment('INSERT_ALL' => 'true')
293
+ end
294
+
295
+ it "should specify insert_all as true if the INSERT_ALL env var is 'TRUE'" do
296
+ expect(SeedDump).to receive(:dump).with(anything, include(insert_all: true))
297
+ SeedDump.dump_using_environment('INSERT_ALL' => 'TRUE')
298
+ end
299
+
300
+ it "should specify insert_all as false if the INSERT_ALL env var is not 'true'" do
301
+ expect(SeedDump).to receive(:dump).with(anything, include(insert_all: false))
302
+ SeedDump.dump_using_environment('INSERT_ALL' => 'false')
303
+ end
304
+
305
+ it "should specify insert_all as false if the INSERT_ALL env var is not set" do
306
+ expect(SeedDump).to receive(:dump).with(anything, include(insert_all: false))
307
+ SeedDump.dump_using_environment
308
+ end
309
+ end
310
+
311
+ describe 'HEADER (issue #126 - comment header in seed file)' do
312
+ # HEADER adds a comment at the top of the seed file showing that seed_dump
313
+ # was used and what options were specified for traceability.
314
+
315
+ it "should specify header as true if the HEADER env var is 'true'" do
316
+ expect(SeedDump).to receive(:dump).with(anything, include(header: true))
317
+ SeedDump.dump_using_environment('HEADER' => 'true')
318
+ end
319
+
320
+ it "should specify header as true if the HEADER env var is 'TRUE'" do
321
+ expect(SeedDump).to receive(:dump).with(anything, include(header: true))
322
+ SeedDump.dump_using_environment('HEADER' => 'TRUE')
323
+ end
324
+
325
+ it "should specify header as false if the HEADER env var is not 'true'" do
326
+ expect(SeedDump).to receive(:dump).with(anything, include(header: false))
327
+ SeedDump.dump_using_environment('HEADER' => 'false')
328
+ end
329
+
330
+ it "should specify header as false if the HEADER env var is not set" do
331
+ expect(SeedDump).to receive(:dump).with(anything, include(header: false))
332
+ SeedDump.dump_using_environment
333
+ end
334
+ end
335
+
336
+ describe 'UPSERT_ALL (issue #104 - non-continuous IDs / foreign key preservation)' do
337
+ # UPSERT_ALL solves the problem where deleted rows cause foreign key
338
+ # references to become invalid after reimporting seeds. When IDs are
339
+ # preserved via upsert_all, foreign key references remain correct.
340
+
341
+ it "should specify upsert_all as true if the UPSERT_ALL env var is 'true'" do
342
+ expect(SeedDump).to receive(:dump).with(anything, include(upsert_all: true))
343
+ SeedDump.dump_using_environment('UPSERT_ALL' => 'true')
344
+ end
345
+
346
+ it "should specify upsert_all as true if the UPSERT_ALL env var is 'TRUE'" do
347
+ expect(SeedDump).to receive(:dump).with(anything, include(upsert_all: true))
348
+ SeedDump.dump_using_environment('UPSERT_ALL' => 'TRUE')
349
+ end
350
+
351
+ it "should specify upsert_all as false if the UPSERT_ALL env var is not 'true'" do
352
+ expect(SeedDump).to receive(:dump).with(anything, include(upsert_all: false))
353
+ SeedDump.dump_using_environment('UPSERT_ALL' => 'false')
354
+ end
355
+
356
+ it "should specify upsert_all as false if the UPSERT_ALL env var is not set" do
357
+ expect(SeedDump).to receive(:dump).with(anything, include(upsert_all: false))
358
+ SeedDump.dump_using_environment
359
+ end
360
+ end
361
+
362
+ it 'should handle non-model classes in ActiveRecord::Base.descendants (issue #112)' do
363
+ # Create a class that inherits from ActiveRecord::Base but doesn't respond to exists?
364
+ # This simulates edge cases like abstract classes or improperly configured models
365
+ non_model_class = Class.new(ActiveRecord::Base) do
366
+ def self.exists?
367
+ raise NoMethodError, "undefined method `exists?' for #{self}"
368
+ end
369
+
370
+ def self.table_exists?
371
+ raise NoMethodError, "undefined method `table_exists?' for #{self}"
372
+ end
373
+ end
374
+ Object.const_set('NonModelClass', non_model_class)
375
+
376
+ allow(SeedDump).to receive(:dump)
377
+
378
+ begin
379
+ expect { SeedDump.dump_using_environment }.not_to raise_error
380
+ ensure
381
+ Object.send(:remove_const, :NonModelClass)
127
382
  end
128
383
  end
129
384
 
130
385
  it 'should run ok without ActiveRecord::SchemaMigration being set (needed for Rails Engines)' do
131
- schema_migration = ActiveRecord::SchemaMigration
386
+ # Ensure Sample model exists before trying to remove SchemaMigration
387
+ expect(defined?(Sample)).to be_truthy
388
+ schema_migration_defined = defined?(ActiveRecord::SchemaMigration)
389
+ schema_migration = ActiveRecord::SchemaMigration if schema_migration_defined
132
390
 
133
- ActiveRecord.send(:remove_const, :SchemaMigration)
391
+ # Stub the dump method before removing the constant
392
+ allow(SeedDump).to receive(:dump)
134
393
 
135
- SeedDump.stub(:dump)
394
+ # Use remove_const carefully only if it's defined
395
+ ActiveRecord.send(:remove_const, :SchemaMigration) if schema_migration_defined
136
396
 
137
397
  begin
138
- SeedDump.dump_using_environment
398
+ expect { SeedDump.dump_using_environment }.not_to raise_error
139
399
  ensure
140
- ActiveRecord.const_set(:SchemaMigration, schema_migration)
400
+ # Ensure the constant is restored only if it was originally defined
401
+ ActiveRecord.const_set(:SchemaMigration, schema_migration) if schema_migration_defined && !defined?(ActiveRecord::SchemaMigration)
402
+ end
403
+ end
404
+
405
+ describe 'HABTM deduplication (issues #26, #114)' do
406
+ # When using has_and_belongs_to_many, Rails creates two auto-generated models
407
+ # that point to the same join table (e.g., User::HABTM_Roles and Role::HABTM_Users).
408
+ # We should only dump one of them to avoid duplicate seed data.
409
+
410
+ it 'should deduplicate HABTM models that share the same table' do
411
+ # Create mock HABTM classes that share the same table_name
412
+ habtm_class_1 = Class.new(ActiveRecord::Base) do
413
+ self.table_name = 'roles_users'
414
+ def self.name; 'User::HABTM_Roles'; end
415
+ def self.to_s; name; end
416
+ end
417
+
418
+ habtm_class_2 = Class.new(ActiveRecord::Base) do
419
+ self.table_name = 'roles_users'
420
+ def self.name; 'Role::HABTM_Users'; end
421
+ def self.to_s; name; end
422
+ end
423
+
424
+ # Temporarily add these to AR descendants by setting constants
425
+ User = Class.new unless defined?(User)
426
+ Role = Class.new unless defined?(Role)
427
+ User.const_set('HABTM_Roles', habtm_class_1)
428
+ Role.const_set('HABTM_Users', habtm_class_2)
429
+
430
+ begin
431
+ # Stub exists? and table_exists? to return true
432
+ allow(habtm_class_1).to receive(:table_exists?).and_return(true)
433
+ allow(habtm_class_1).to receive(:exists?).and_return(true)
434
+ allow(habtm_class_2).to receive(:table_exists?).and_return(true)
435
+ allow(habtm_class_2).to receive(:exists?).and_return(true)
436
+
437
+ # Track which models get dumped
438
+ dumped_models = []
439
+ allow(SeedDump).to receive(:dump) do |model, _options|
440
+ dumped_models << model.to_s
441
+ end
442
+
443
+ SeedDump.dump_using_environment
444
+
445
+ # Only one of the HABTM models should be dumped, not both
446
+ habtm_dumps = dumped_models.select { |m| m.include?('HABTM_') }
447
+ habtm_tables = habtm_dumps.map { |m| m.include?('HABTM_Roles') ? 'roles_users' : 'roles_users' }
448
+
449
+ expect(habtm_dumps.size).to eq(1), "Expected 1 HABTM model to be dumped, got #{habtm_dumps.size}: #{habtm_dumps}"
450
+ ensure
451
+ User.send(:remove_const, 'HABTM_Roles') if defined?(User::HABTM_Roles)
452
+ Role.send(:remove_const, 'HABTM_Users') if defined?(Role::HABTM_Users)
453
+ Object.send(:remove_const, 'User') if defined?(User) && User.is_a?(Class) && User.superclass == Object
454
+ Object.send(:remove_const, 'Role') if defined?(Role) && Role.is_a?(Class) && Role.superclass == Object
455
+ end
456
+ end
457
+
458
+ it 'should not affect non-HABTM models with different tables' do
459
+ # Sample and AnotherSample have different tables, so both should dump
460
+ allow(SeedDump).to receive(:dump)
461
+
462
+ FactoryBot.create(:another_sample)
463
+ expect(SeedDump).to receive(:dump).with(Sample, anything)
464
+ expect(SeedDump).to receive(:dump).with(AnotherSample, anything)
465
+
466
+ SeedDump.dump_using_environment
467
+ end
468
+ end
469
+
470
+ describe 'foreign key dependency ordering (issues #78, #83)' do
471
+ # Models with foreign key dependencies should be dumped in the correct order
472
+ # so that seeds can be imported without foreign key violations.
473
+ # For example: Author -> Book -> Review means Author should be dumped first,
474
+ # then Book, then Review.
475
+
476
+ it 'should order models by foreign key dependencies' do
477
+ # Create records with dependencies
478
+ author = FactoryBot.create(:author)
479
+ book = FactoryBot.create(:book, author: author)
480
+ FactoryBot.create(:review, book: book)
481
+
482
+ # Track which models get dumped and in what order
483
+ dumped_models = []
484
+ allow(SeedDump).to receive(:dump) do |model, _options|
485
+ dumped_models << model.to_s
486
+ end
487
+
488
+ SeedDump.dump_using_environment('MODELS' => 'Review,Book,Author')
489
+
490
+ # Verify the order: Author must come before Book, Book must come before Review
491
+ author_index = dumped_models.index('Author')
492
+ book_index = dumped_models.index('Book')
493
+ review_index = dumped_models.index('Review')
494
+
495
+ expect(author_index).not_to be_nil, "Author should be in the dump"
496
+ expect(book_index).not_to be_nil, "Book should be in the dump"
497
+ expect(review_index).not_to be_nil, "Review should be in the dump"
498
+
499
+ expect(author_index).to be < book_index,
500
+ "Author (index #{author_index}) should be dumped before Book (index #{book_index})"
501
+ expect(book_index).to be < review_index,
502
+ "Book (index #{book_index}) should be dumped before Review (index #{review_index})"
503
+ end
504
+
505
+ it 'should handle models without foreign key dependencies' do
506
+ # Sample has no foreign keys, should still be dumped normally
507
+ FactoryBot.create(:author)
508
+
509
+ dumped_models = []
510
+ allow(SeedDump).to receive(:dump) do |model, _options|
511
+ dumped_models << model.to_s
512
+ end
513
+
514
+ SeedDump.dump_using_environment('MODELS' => 'Sample,Author')
515
+
516
+ expect(dumped_models).to include('Sample')
517
+ expect(dumped_models).to include('Author')
518
+ end
519
+
520
+ it 'should handle circular dependencies gracefully' do
521
+ # Create models with circular dependency for testing
522
+ # PersonA belongs_to PersonB, PersonB belongs_to PersonA
523
+ person_a_class = Class.new(ActiveRecord::Base) do
524
+ self.table_name = 'person_as'
525
+ end
526
+ person_b_class = Class.new(ActiveRecord::Base) do
527
+ self.table_name = 'person_bs'
528
+ end
529
+ Object.const_set('PersonA', person_a_class)
530
+ Object.const_set('PersonB', person_b_class)
531
+
532
+ # Add circular associations after both classes exist
533
+ PersonA.belongs_to :person_b, optional: true
534
+ PersonB.belongs_to :person_a, optional: true
535
+
536
+ # Create tables
537
+ ActiveRecord::Schema.define do
538
+ create_table 'person_as', force: true do |t|
539
+ t.references :person_b
540
+ end
541
+ create_table 'person_bs', force: true do |t|
542
+ t.references :person_a
543
+ end
544
+ end
545
+
546
+ # Create records
547
+ PersonA.create!
548
+ PersonB.create!
549
+
550
+ begin
551
+ dumped_models = []
552
+ allow(SeedDump).to receive(:dump) do |model, _options|
553
+ dumped_models << model.to_s
554
+ end
555
+
556
+ # Should not raise an error despite circular dependency
557
+ expect {
558
+ SeedDump.dump_using_environment('MODELS' => 'PersonA,PersonB')
559
+ }.not_to raise_error
560
+
561
+ # Both models should be dumped
562
+ expect(dumped_models).to include('PersonA')
563
+ expect(dumped_models).to include('PersonB')
564
+ ensure
565
+ Object.send(:remove_const, :PersonA)
566
+ Object.send(:remove_const, :PersonB)
567
+ end
568
+ end
569
+ end
570
+
571
+ describe 'STI deduplication (issue #120)' do
572
+ # When using STI (Single Table Inheritance), multiple model classes share
573
+ # the same database table. For example, AdminUser < BaseUser and
574
+ # GuestUser < BaseUser all use the 'base_users' table.
575
+ # Without deduplication, each STI subclass would be dumped separately,
576
+ # creating duplicate records in the seeds file.
577
+
578
+ it 'should deduplicate STI models by keeping only the base class' do
579
+ # Create records of different STI types
580
+ FactoryBot.create(:admin_user)
581
+ FactoryBot.create(:guest_user)
582
+
583
+ # Track which models get dumped
584
+ dumped_models = []
585
+ allow(SeedDump).to receive(:dump) do |model, _options|
586
+ dumped_models << model.to_s
587
+ end
588
+
589
+ SeedDump.dump_using_environment
590
+
591
+ # Only BaseUser should be dumped, not AdminUser or GuestUser
592
+ expect(dumped_models).to include('BaseUser')
593
+ expect(dumped_models).not_to include('AdminUser')
594
+ expect(dumped_models).not_to include('GuestUser')
595
+ end
596
+
597
+ it 'should dump all records through the base class' do
598
+ # Create records of different STI types
599
+ FactoryBot.create(:admin_user)
600
+ FactoryBot.create(:guest_user)
601
+ FactoryBot.create(:base_user)
602
+
603
+ # The base class should have access to all records
604
+ result = SeedDump.dump(BaseUser)
605
+
606
+ # All three records should be in the output
607
+ expect(result).to include('AdminUser')
608
+ expect(result).to include('GuestUser')
609
+ expect(result).to include('BaseUser')
141
610
  end
142
611
  end
143
- end
144
- end
612
+ end
613
+ end